<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><link rel="stylesheet" type="text/css" href="http://storage.360buyimg.com/ebooks/9fd8bb77eb40456b746aaae41785499a_new_.css" /><title>第15章 测试</title></head><body><p class="center"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t17836/196/2670436298/31991/773816d/5b03d540N0957c468.jpg" class="fullscreen" alt="" href="./image/Figure-015.jpg" /></p><p class="content">经过夜以继日的奋战，终于熬到可以对外发布的日子了。是时候让过去投入的大量精力和时间得到回报了。然而，传来的一个消息犹如晴天霹雳：一个致命的bug导致用户无法注册。</p><p class="sectionTitle biaoti3"><a id="sec001"></a>15.1 测试驱动？</p><p class="content">测试能够防患于未然，提升对程序的信心，也可以为新加入的开发人员提供指引。在软件开发领域，几乎没人质疑测试的作用。但是，人们在如何测试这个问题上一直争论不休。</p><p class="content">一种方法是先写测试，再写实现过程，直至测试通过；另一种是已有实现代码，再写测试，验证代码是否正确。令人不解的是，二者的合理性常在开发社区中引发口水战。双方僵持不下，争论哪个才是<b>正确</b>的方法。</p><p class="content">基于以往的经验，尤其是在严重依赖原型的情况下，我们将重点放在构建可测试代码上。我们发现，即使你的经历有所不同，但是在构建原型时，测试可能经常变更的代码片断会比让它运行起来耗费2～3倍的工作量。与此相反，我们在构建基于小型组件的应用程序时，将大量功能分解成不同的方法，从而测试整个蓝图的部分功能。这就是我们所说的<b>可测试</b>代码。</p><p class="reference"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t20827/10/215994487/12231/293f6125/5b03d541Ndff69dec.jpg" class="inline1" alt="" href="./image/a-3.jpg" />　一种替代构建原型（后测试）的方法论便是所谓的“红色—绿色—重构”<sup><a class="mz-footnote-link" href="#footnote1"><img alt="" src="https://img30.360buyimg.com/ebookadmin/jfs/t21616/337/214486965/1481/90828627/5b03d540N24da8434.png" href="./image/icon_note.png" /></a></sup>。它的理念是要求你先写测试。运行测试会得到失败结果（红色），因为你还没有写任何实现的代码。只有在测试失败之后，才去写实现代码，直至所有测试通过（绿色）。</p><p class="content">当然，测试<b>什么</b>取决于你和你的团队，而本章的重点在于讨论<b>如何</b>测试程序。</p><p class="sectionTitle biaoti3"><a id="sec002"></a>15.2 端对端测试与单元测试</p><p class="content">测试程序有两种主要方法：<b>端对端测试</b>和<b>单元测试</b>。</p><p class="content">如果使用自上而下的方法进行测试，那么写测试时就将程序视为一个“黑盒”。与程序交互就如真实用户一样，从“旁观者”的角度评判程序是否达标。这种自上而下的测试技巧被称为<b>端对端测试</b>。</p><p class="reference"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t20827/10/215994487/12231/293f6125/5b03d541Ndff69dec.jpg" class="inline1" alt="" href="./image/a-3.jpg" />　在Angular中，最常用的工具叫作Protractor<sup><a class="mz-footnote-link" href="#footnote2"><img alt="" src="https://img30.360buyimg.com/ebookadmin/jfs/t21616/337/214486965/1481/90828627/5b03d540N24da8434.png" href="./image/icon_note.png" /></a></sup>。Protractor能够打开浏览器与程序交互，收集测试结果，并检验测试结果与预期值是否相符。</p><p class="content">第二种常用的测试方法是隔离程序的每个部件，在隔离环境中运行测试。这种测试形式叫作<b>单元测试</b>。</p><p class="content">在单元测试中，所写的测试需要事先提供既定的输入值与相应的逻辑单元，检测输出结果，确定它是否与我们的预期结果匹配。</p><p class="content">在本章中，我们将会探讨如何对Angular程序进行<b>单元测试</b>。</p><p class="sectionTitle biaoti3"><a id="sec003"></a>15.3 测试工具</p><p class="content">为了测试程序，我们将用到两种工具：Jasmine和Karma。</p><p class="listTitle1 biaoti8">15.3.1 Jasmine</p><p class="content">Jasmine<sup><a class="mz-footnote-link" href="#footnote3"><img alt="" src="https://img30.360buyimg.com/ebookadmin/jfs/t21616/337/214486965/1481/90828627/5b03d540N24da8434.png" href="./image/icon_note.png" /></a></sup>是一种用于测试JavaScript代码的行为驱动框架。</p><p class="content">利用Jasmine，你可以设置代码在调用后的预期结果。</p><p class="content">比如，我们假定Calculator对象有一个sum函数。想确保1加1的结果为2，就可以用一个测试（也叫<b>规格</b>，spec）来表达。使用以下代码：</p><p class="daima">describe（'Calculator',（）=&gt; {</p><p class="daima">　it（'sums 1 and 1 to 2',（）=&gt; {</p><p class="daima">　　var calc = new Calculator（）;</p><p class="daima">　　expect（calc.sum（1, 1））.toEqual（2）;</p><p class="daima">　}）;</p><p class="daima">}）;</p><p class="content">使用Jasmine的一个优点是代码易于阅读。从以上代码可以看到，我们期望 calc.sum的结果等于2。</p><p class="content">测试通常由多个describe块和it块组成。</p><p class="content">通常，我们用describe来组织要测试的逻辑单元，对于其内部每个要使用断言的预期都会用到一个it块。然而，这并不是一个硬性规定。你会经常看到一个it块包含多个预期。</p><p class="content">在上述Calculator示例中，我们只是列举了一个简单的对象。正因为如此，整个类只使用了一个describe块，而每个方法使用一个it块。</p><p class="content">大多数情况下并非如此。比如，某些方法会根据不同输入值产生不同结果，那么这些方法可以拥有多个相应的it块。在这种情况下，最好使用嵌套的describe块：对象级别用一个，每个方法也各用一个，然后在其内部的每个断言语句用一个单独的it块包裹。</p><p class="content">大量有关describe块和it块的示例将贯穿本章。不必烦恼到底该用describe块还是it块，我们将用大量示例演示说明。</p><p class="content">更多有关Jasmine和其语法的资料，参见Jasmine官方文档：http：//jasmine.github.io/2.4/introduction.html。</p><p class="listTitle1 biaoti8">15.3.2 Karma</p><p class="content">使用Jasmine，我们可以描述测试和预期结果。要运行测试，还需要为测试提供一个浏览器环境。</p><p class="content">Karma应运而生。使用Karma，我们可以在Chrome或Firefox之类的真实浏览器或者PhantomJS这样的<b>空壳</b>浏览器（无用户界面）内运行JavaScript代码。</p><p class="sectionTitle biaoti3"><a id="sec004"></a>15.4 编写单元测试</p><p class="content">本节的重点是理解如何对一个Angular程序的各个部件进行单元测试。</p><p class="content">我们将会学习如何测试<b>服务</b>、<b>组件</b>、<b>HTTP请求</b>等。同时，我们也会收获一些小技巧，让代码更容易测试。</p><p class="sectionTitle biaoti3"><a id="sec005"></a>15.5 Angular单元测试框架</p><p class="content">Angular自身提供了一套基于Jasmine框架的辅助类，用以帮助我们编写单元测试。</p><p class="content">主要的测试框架位于@angular/core/testing包中。（然而，为了测试组件，我们会用到 @angular/compiler/testing包和@angular/platform-browser/testing包中的一些辅助类。稍后具体介绍。）</p><p class="center"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t20827/10/215994487/12231/293f6125/5b03d541Ndff69dec.jpg" class="enlarge" alt="" href="./image/a-3.jpg" /></p><p class="content">如果这是你初次测试Angular程序，那么在为Angular写单元测试时，需要先完成一些必要的设置步骤。</p><p class="content">例如，在需要注入依赖时，我们经常手动配置它们。在测试一个组件时，需要使用测试辅助类初始化它们。在测试路由时，还需要构建一些依赖。</p><p class="content">设置有些繁琐，但不用太担心。一旦掌握，你就会发现从一个项目切换到另外一个项目，配置不会有多大变化。另外，本章也会指引你完成每一步。</p><p class="content">和往常一样，可以在代码下载页面获取本章所有的源代码。用你喜欢的编辑器直接打开浏览，可以对本章涵盖的细节有一个大体的把握。我们建议你坚持参照代码来阅读本章。</p><p class="sectionTitle biaoti3"><a id="sec006"></a>15.6 测试前准备</p><p class="content">我们在第7章创建了一个用于搜索音乐的应用。本章开始为这个程序编写测试。</p><p class="content">Karma需要一个配置文件才能运行。因此配置Karma的第一步就是创建一个karma.conf.js文件。</p><p class="content">将karma.conf.js放在项目的根目录下，如下所示。</p><p class="reference"><b>code/routes/music/karma.conf.js</b></p><p class="daima">// Karma configuration</p><p class="daima">var path = require（'path'）;</p><p class="daima">var cwd = process.cwd（）;</p><p class="daima">module.exports = function（config）{</p><p class="daima">　config.set（{</p><p class="daima">　　// base path that will be used to resolve all patterns（eg．files, exclude）</p><p class="daima">　　basePath：'',</p><p class="daima">　　// frameworks to use</p><p class="daima">　　// available frameworks：https：//npmjs.org/browse/keyword/karma-adapter</p><p class="daima">　　frameworks：['jasmine'],</p><p class="daima">　　// list of files / patterns to load in the browser</p><p class="daima">　　files：[</p><p class="daima">　　　{ pattern：'test.bundle.js', watched：false }</p><p class="daima">　　],</p><p class="daima">　　// list of files to exclude</p><p class="daima">　　exclude：[</p><p class="daima">　　],</p><p class="daima">　　// preprocess matching files before serving them to the browser</p><p class="daima">　　// available preprocessors：https：//npmjs.org/browse/keyword/karma-preproces\</p><p class="daima">sor</p><p class="daima">　　preprocessors：{</p><p class="daima">　　　'test.bundle.js'：['webpack', 'sourcemap']</p><p class="daima">　　},</p><p class="daima">　　webpack：{</p><p class="daima">　　　devtool：'inline-source-map',</p><p class="daima">　　　resolve：{</p><p class="daima">　　　　root：[path.resolve（cwd）],</p><p class="daima">　　　　modulesDirectories：['node_modules', 'app', 'app/ts', 'test', '.'],</p><p class="daima">　　　　extensions：['', '.ts', '.js', '.css'],</p><p class="daima">　　　　alias：{</p><p class="daima">　　　　　'app'：'app'</p><p class="daima">　　　　}</p><p class="daima">　　　},</p><p class="daima">　　　module：{</p><p class="daima">　　　　loaders：[</p><p class="daima">　　　　　{ test：/\.ts$/, loader：'ts-loader', exclude：[/node_modules/]}</p><p class="daima">　　　　]</p><p class="daima">　　　},</p><p class="daima">　　　stats：{</p><p class="daima">　　　　colors：true,</p><p class="daima">　　　　reasons：true</p><p class="daima">　　　},</p><p class="daima">　　　watch：true,</p><p class="daima">　　　debug：true</p><p class="daima">　　},</p><p class="daima">　　webpackServer：{</p><p class="daima">　　　noInfo：true</p><p class="daima">　　},</p><p class="daima">　　// test results reporter to use</p><p class="daima">　　// possible values：'dots', 'progress'</p><p class="daima">　　// available reporters：https：//npmjs.org/browse/keyword/karma-reporter</p><p class="daima">　　reporters：['spec'],</p><p class="daima">　　// web server port</p><p class="daima">　　port：9876,</p><p class="daima">　　// enable / disable colors in the output（reporters and logs）</p><p class="daima">　　colors：true,</p><p class="daima">　　// level of logging</p><p class="daima">　　// possible values：config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WAR\</p><p class="daima">N || config.LOG_INFO || config.LOG_DEBUG</p><p class="daima">　　logLevel：config.LOG_INFO,</p><p class="daima">　　// enable / disable watching file and executing tests whenever any file chan\</p><p class="daima">ges</p><p class="daima">　　autoWatch：true,</p><p class="daima">　　// start these browsers</p><p class="daima">　　// available browser launchers：https：//npmjs.org/browse/keyword/karma-launc\</p><p class="daima">her</p><p class="daima">　　browsers：['Chrome'],</p><p class="daima">　　// Continuous Integration mode</p><p class="daima">　　// if true, Karma captures browsers, runs the tests and exits</p><p class="daima">　　singleRun：false</p><p class="daima">　}）</p><p class="daima">}</p><p class="content">先别急于弄清这个文件的内容，而是记住以下几点：</p><p class="content">●将PhantomJS设置成目标测试浏览器；</p><p class="content">●使用Jasmine karma框架进行测试；</p><p class="content">●使用一个名为test.bundle.js的webpack bundle文件包裹所有的测试和程序代码。</p><p class="content">下一步，新建一个名为test的文件夹，用于存放测试文件：</p><p class="daima">mkdir test</p><p class="sectionTitle biaoti3"><a id="sec007"></a>15.7 测试服务类和HTTP</p><p class="content">服务类在Angular程序中常以普通类的形式出现。在某种意义上说，这简化了测试，因为有时可以在不需要Angular的情况下直接进行测试。</p><p class="content">配置好Karma，就可以开始测试SpotifyService类了。如果记得没错，这个服务类通过与Spotify API交互读取专辑、曲目和艺术家相关信息。</p><p class="content">切换到test文件夹，新建一个service子文件夹，用于存放即将开始的服务类测试。一切准备就绪，开始创建第一个服务类测试文件，名为SpotifyService.spec.ts。</p><p class="content">下面开始组织这个测试文件。首先需要从@angular/core/testing包中导入几个辅助类。</p><p class="reference"><b>code/routes/music/test/services/SpotifyService.spec.ts</b></p><p class="daima">import {</p><p class="daima">　inject,</p><p class="daima">　fakeAsync,</p><p class="daima">　tick,</p><p class="daima">　TestBed</p><p class="daima">} from '@angular/core/testing';</p><p class="content">接下来，还需要导入其他几个类。</p><p class="reference"><b>code/routes/music/test/services/SpotifyService.spec.ts</b></p><p class="daima">import {MockBackend} from '@angular/http/testing';</p><p class="daima">import {</p><p class="daima">　Http,</p><p class="daima">　ConnectionBackend,</p><p class="daima">　BaseRequestOptions,</p><p class="daima">　Response,</p><p class="daima">　ResponseOptions</p><p class="daima">} from '@angular/http';</p><p class="content">既然我们的服务用到了HTTP请求，就需要从@angular/http/testing包中导入MockBackend。有了这个类，就可以设置预期值和验证HTTP请求结果了。</p><p class="content">最后，导入我们要测试目标类。</p><p class="reference"><b>code/routes/music/test/services/SpotifyService.spec.ts</b></p><p class="daima">import {SpotifyService} from '../../app/ts/services/SpotifyService';</p><p class="listTitle1 biaoti8">15.7.1 HTTP要点</p><p class="content">马上要编写测试了，但是在每个测试的运行过程中都要访问Spotify服务器。这显然有些不妥，原因如下。</p><p class="content">（1）HTTP请求相对比较慢，而且随着测试套件的体积越来越大，可以预见运行全部测试需要的时间也会越来越长。</p><p class="content">（2）Spotify的API调用设置了阈值限制，如果不停地运行测试，会很快耗尽所有的API调用资源。</p><p class="content">（3）如果处于离线状态、Spotify崩溃或无法访问，那么测试也会失败，即使代码在技术角度上没有问题也是一样。</p><p class="content">这在写单元测试时给了我们一个提示：在运行测试前，必须隔离那些无法掌控的东西。</p><p class="content">在我们例子中，对应的就是Spotify服务。解决方法是，用一个替身替换掉HTTP请求，而且这个替身<b>不需要访问真实的Spotify服务器</b>。</p><p class="content">在测试领域，这个过程被称为<b>模拟</b>依赖，也时也叫作<b>伪装</b>依赖。</p><p class="reference"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t18559/108/2587195628/17417/8ceb743d/5b03d540N559a889f.jpg" class="inline1" alt="" href="./image/a-9.jpg" />　阅读文章“模拟不是伪装”（http：//martinfowler.com/articles/mocksArentStubs.html）可以了解更多有关模拟和伪装之间的差异。</p><p class="content">假设我们正在写的测试依赖于某个Car类。</p><p class="content">它有几个方法：你可以调用start来启动一个Car实例，也可以调用汽车的其他方法，如 stop（停车），park（泊车）和 getSpeed（读取车速）。</p><p class="content">下面介绍如何使用伪装和模拟来写依赖于这个类的测试。</p><p class="listTitle1 biaoti8">15.7.2 伪装</p><p class="content"><b>伪装</b>是即时创建的对象，它包含所依赖对象所有行为的一个子集。</p><p class="content">下面写一个测试与start方法交互。</p><p class="content">为Car即时创建一个<b>伪装</b>并将它注入到要测试的类中：</p><p class="daima">describe（'Speedtrap', function（）{</p><p class="daima">　it（'tickets a car at more than 60mph', function（）{</p><p class="daima">　　var stubCar = { getSpeed：function（）{ return 61; } };</p><p class="daima">　　var speedTrap = new SpeedTrap（stubCar）;</p><p class="daima">　　speedTrap.ticketCount = 0;</p><p class="daima">　　speedTrap.checkSpeed（）;</p><p class="daima">　　expect（speedTrap.ticketCount）.toEqual（1）;</p><p class="daima">　}）;</p><p class="daima">}）;</p><p class="content">这是使用伪装的一个典型场景。我们可能仅仅在某个测试内部使用它。</p><p class="listTitle1 biaoti8">15.7.3 模拟</p><p class="content">在我们的例子中，<b>模拟</b>是对象更完整的体现，它会重写依赖的部分或全部行为。在大部分情况下，模拟可以在一个测试套件的多个测试间反复使用。</p><p class="content">它们有时用于断言方法是否按预期的方式调用。</p><p class="content">一个模拟版本的Car类可能是这样的：</p><p class="daima">class MockCar {</p><p class="daima">　startCallCount：number = 0;</p><p class="daima">　start（）{</p><p class="daima">　　this.startCallCount++;</p><p class="daima">　}</p><p class="daima">}</p><p class="content">它可以用在另外一个测试中，如：</p><p class="daima">describe（'CarRemote', function（）{</p><p class="daima">　it（'starts the car when the start key is held', function（）{</p><p class="daima">　　var car = new MockCar（）;</p><p class="daima">　　var remote = new CarRemote（）;</p><p class="daima">　　remote.holdButton（'start'）;</p><p class="daima">　　expect（car.startCallCount）.toEqual（1）;</p><p class="daima">　}）;</p><p class="daima">}）;</p><p class="content">模拟和伪装的最大区别在于：</p><p class="content">●伪装提供手动重写行为功能的一个子集；</p><p class="content">●模拟通常预设期望值，验证调用某些方法的返回结果。</p><p class="listTitle1 biaoti8">15.7.4 Http MockBackend</p><p class="content">既然现在心里有底了，就继续编写之前的服务类测试代码。</p><p class="content">每次运行测试时都与在线的Spotify服务进行交互显然不是个好主意。幸运的是Angular提供了一种方法，使用MockBackend来伪装HTTP调用。</p><p class="content">可以将这个类注入到一个Http实例中，这样我们就能按照自己的意图对HTTP交互行为进行操控了。可以使用不同的方法进行干预和断言：手动设置响应，模拟HTTP错误，添加更多预期（比如判断请求的URL是否与预期值匹配，请求参数是否正确，等等）。</p><p class="content">因此这里的想法就是使用一个伪HTTP库。这个伪HTTP看起来和真实的HTTP库一样：所有方法一一匹配，可以返回响应结果，等等。然而，我们却不会<b>真正</b>发出一条请求。</p><p class="content">事实上，除了伪造请求外，MockBackend还允许我们设置<b>期望结果</b>，监控我们的预期行为。</p><p class="listTitle1 biaoti8">15.7.5 TestBed.configureTestingModule和提供者</p><p class="content">当测试Angular程序时，需要确保配置顶级NgModule，后面会在这个测试中用到它。在进行配置时，我们要配置提供者、声明组件并导入其他模块：就像你平常使用NgModule一样（参见8.10节）。</p><p class="content">测试Angular代码时，我们有时采取<b>手动设置注入</b>的方式。这样做的好处是能够对测试进行更多的操控。</p><p class="content">所以在测试Http请求时，我们不会注入一个“真实”的Http类，取而代之的是注入一个看起来像Http的替身，但它可以真实地拦截请求，返回我们事先配置的响应。</p><p class="content">为了做到这一点，要创建一个Http变体，其内部使用MockBackend。</p><p class="content">方法是，在beforeEach钩子中使用TestBed.configureTestingModule。这个钩子接收一个回调函数，它会在每个测试运行之前被调用。这为替换类的具体实现提供了一个难得的机会。</p><p class="reference"><b>code/routes/music/test/services/SpotifyService.spec.ts</b></p><p class="daima">describe（'SpotifyService',（）=&gt; {</p><p class="daima">　beforeEach（（）=&gt; {</p><p class="daima">　　TestBed.configureTestingModule（{</p><p class="daima">　　　providers：[</p><p class="daima">　　　　BaseRequestOptions,</p><p class="daima">　　　　MockBackend,</p><p class="daima">　　　　SpotifyService,</p><p class="daima">　　　　{ provide：Http,</p><p class="daima">　　　　　useFactory：（backend：ConnectionBackend,</p><p class="daima">　　　　　　　　　　　 defaultOptions：BaseRequestOptions）=&gt; {</p><p class="daima">　　　　　　　　　　　　 return new Http（backend, defaultOptions）;</p><p class="daima">　　　　　　　　　　　 }, deps：[MockBackend, BaseRequestOptions] },</p><p class="daima">　　　]</p><p class="daima">　　}）;</p><p class="daima">　}）;</p><p class="content">注意TestBed.configureTestingModule的providers参数可以接收<b>提供者数组</b>，用于测试注入器。</p><p class="content">BaseRequestOptions和SpotifyService是那些类的默认实现。最后一个提供者有点复杂。</p><p class="reference"><b>code/routes/music/test/services/SpotifyService.spec.ts</b></p><p class="daima">　　　　{ provide：Http,</p><p class="daima">　　　　　useFactory：（backend：ConnectionBackend,</p><p class="daima">　　　　　　　　　　　 defaultOptions：BaseRequestOptions）=&gt; {</p><p class="daima">　　　　　　　　　　　　 return new Http（backend, defaultOptions）;</p><p class="daima">　　　　　　　　　　　 }, deps：[MockBackend, BaseRequestOptions] },</p><p class="daima">　　　]</p><p class="content">这段代码使用了provide和useFactory参数来创建一个Http类变体，使用了工厂模式（也就是useFactory的职责所在）。</p><p class="content">这个工厂的方法签名需要接收一个ConnectionBackend实例和一个BaseRequestOption实例。这个对象的第二个参数是deps：[MockBackend, BaseRequestOptions]。这表示MockBackend是工厂的第一个参数，BaseRequestOptions（默认实现）为第二个参数。</p><p class="content">最后，返回一个MockBackend作为函数结果的定制Http类。</p><p class="content">这样做有什么好处呢？在测试代码中每次需要注入Http的地方，得到的都是我们改装过的Http实例。</p><p class="content">我们会在大量测试中使用这个行之有效的方法：用依赖注入的方法定制依赖，隔离需要测试的功能。</p><p class="listTitle1 biaoti8">15.7.6 测试getTrack方法</p><p class="content">下面针对这个服务类写一个测试，验证我们正在调用正确的URL。</p><p class="reference"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t18559/108/2587195628/17417/8ceb743d/5b03d540N559a889f.jpg" class="inline1" alt="" href="./image/a-9.jpg" />　如果你还没看过第7章的音乐程序，可以在7.10.5节找到源代码。</p><p class="content">现在开始测试getTrack方法。</p><p class="reference"><b>code/routes/music/app/ts/services/SpotifyService.ts</b></p><p class="daima">　getTrack（id：string）：Observable&lt;any[]&gt; {</p><p class="daima">　　return this.query（`/tracks/${id}`）;</p><p class="daima">　}</p><p class="content">你可能还记得这个方法的细节，它调用了query方法，从而分析接收的参数并拼接成URL。</p><p class="reference"><b>code/routes/music/app/ts/services/SpotifyService.ts</b></p><p class="daima">　query（URL：string, params?：Array&lt;string&gt;）：Observable&lt;any[]&gt; {</p><p class="daima">　　let queryURL：string = `${SpotifyService.BASE_URL}${URL}`;</p><p class="daima">　　if（params）{</p><p class="daima">　　　queryURL = `${queryURL}?${params.join（'&amp;'）}`;</p><p class="daima">　　}</p><p class="daima">　　return this.http.request（queryURL）.map（（res：any）=&gt; res.json（））;</p><p class="daima">　}</p><p class="content">请求/tracks/${id}意味着假设当调用getTrack（'TRACK_ID'）方法时，期望返回的URL是https：//api.spotify.com/v1/tracks/TRACK_ID。</p><p class="content">可以这样写这个测试：</p><p class="daima">describe（'getTrack',（）=&gt; {</p><p class="daima">　it（'retrieves using the track ID',</p><p class="daima">　　inject（[SpotifyService, MockBackend], fakeAsync（（spotifyService, mockBackend）=&gt; {</p><p class="daima">　　　var res;</p><p class="daima">　　　mockBackend.connections.subscribe（c =&gt; {</p><p class="daima">　　　　expect（c.request.url）.toBe（'https：//api.spotify.com/v1/tracks/TRACK_ID'）;</p><p class="daima">　　　　let response = new ResponseOptions（<b>ody：'{"name"："felipe"</b>'}）;</p><p class="daima">　　　　c.mockRespond（new Response（response））;</p><p class="daima">　　　}）;</p><p class="daima">　　　spotifyService.getTrack（'TRACK_ID'）.subscribe（（_res）=&gt; {</p><p class="daima">　　　　res = _res;</p><p class="daima">　　　}）;</p><p class="daima">　　　tick（）;</p><p class="daima">　　　expect（res.name）.toBe（'felipe'）;</p><p class="daima">　　}））</p><p class="daima">　）;</p><p class="daima">}）;</p><p class="content">初看有点难以理解，下面一一讲解。</p><p class="content">当测试有依赖时，使用Angular注入器提供那些类的实例。如下所示：</p><p class="daima">inject（[Class1, ……, ClassN],（instance1, ……, instanceN）=&gt; {</p><p class="daima">　…… testing code ……</p><p class="daima">}）</p><p class="content">当测试返回的是一个承诺或者RxJS的可观察对象时，可以使用fakeAsync辅助工具来测试那些代码（像测试同步代码那样）。在调用tick（）后，承诺立即生效，可观察对象也会马上接收到通知。</p><p class="content">如下列代码所示：</p><p class="daima">inject（[SpotifyService, MockBackend], fakeAsync（（spotifyService, mockBackend）=&gt; {</p><p class="daima">　……</p><p class="daima">}））;</p><p class="content">首先要读取两个变量：spotifyService和mockBackend。前者是一个特定的SpotifyService实例，后者是一个MockBackend实例。注意内部函数（spotifyService, mockBackend）的参数是注入的，相应的类型在inject函数第一个参数的数组中（SpotifyService和MockBackend）指定。</p><p class="content">其次运行位于fakeAsync内部的代码。这就意味着当调用tick（）时，异步代码会以同步方式运行。</p><p class="content">测试的运行环境已经准备就绪，现在可以写“真正”的测试代码了。首先声明一个res变量，存放HTTP调用响应结果。然后，订阅mockBackend.connections事件：</p><p class="daima">var res;</p><p class="daima">mockBackend.connections.subscribe（c =&gt; { …… }）;</p><p class="content">简单地说，每当mockBackend上产生一个新的连接，我们都希望收到通知（例如，调用了这个函数）。</p><p class="content">为了验证SpotifyService根据指定的TRACK_ID调用了正确的URL，可以指定<b>预期结果</b>为我们预设的URL。首先通过c.request.url得到URL值，然后设置期望结果：c.request.url的值应该是字符串'https：//api.spotify.com/v1/tracks/TRACK_ID'：</p><p class="daima">expect（c.request.url）.toBe（'https：//api.spotify.com/v1/tracks/TRACK_ID'）;</p><p class="content">运行测试。如果请求URL不匹配，则测试失败。</p><p class="content">现在我们已经收到了请求，并证明了它是正确的。现在需要打造一个响应。为此，新建一个ResponseOptions实例，指定JSON字符串{"name"："felipe"}为响应内容。</p><p class="daima">let response = new ResponseOptions（<b>ody：'{"name"："felipe"</b>'}）;</p><p class="content">最后，将连接的响应替换成一个Response对象，它包裹了刚刚创建的ResponseOptions实例。</p><p class="daima">c.mockRespond（new Response（response））;</p><p class="reference"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t18559/108/2587195628/17417/8ceb743d/5b03d540N559a889f.jpg" class="inline1" alt="" href="./image/a-9.jpg" />　注意，subscribe中的回调函数可以复杂到任何你想要的程度，可以包含基于URL的条件逻辑、查询参数或者任何可以从请求对象中读取的信息。</p><p class="content">这样一来，我们就可以为可能遇到的每个场景编写测试了。</p><p class="content">现在已经准备好了使用TRACK_ID参数来调用getTrack方法，并且可以通过res变量跟踪响应结果：</p><p class="daima">spotifyService.getTrack（'TRACK_ID'）.subscribe（（_res）=&gt; {</p><p class="daima">　res = _res;</p><p class="daima">}）;</p><p class="content">如果此时中断测试，在触发回调函数前会一直等待HTTP请求发送和响应结果返回。也有可能产生其他执行路径，我们不得不重新组织代码对任务进行同步。幸好fakeAsync可以解决这个问题。方法是调用tick（），异步代码会立即执行，就像变魔术一样：</p><p class="daima">tick（）;</p><p class="content">执行最后一步检验，确保设置的响应结果和接收到的相同：</p><p class="daima">expect（res.name）.toBe（'felipe'）;</p><p class="content">细想一下，这个服务类的所有方法的代码都非常类似。将设置URL预期值的代码片断抽取出来，放到一个名为expectedURL的函数中。</p><p class="reference"><b>code/routes/music/test/services/SpotifyService.spec.ts</b></p><p class="daima">　function expectURL（backend：MockBackend, url：string）{</p><p class="daima">　　backend.connections.subscribe（c =&gt; {</p><p class="daima">　　　expect（c.request.url）.toBe（url）;</p><p class="daima">　　　let response = new ResponseOptions（<b>ody：'{"name"："felipe"</b>'}）;</p><p class="daima">　　　c.mockRespond（new Response（response））;</p><p class="daima">　　}）;</p><p class="daima">　}</p><p class="content">依葫芦画瓢，可以轻而易举地为getArtist和getAlbum方法编写测试。</p><p class="reference"><b>code/routes/music/test/services/SpotifyService.spec.ts</b></p><p class="daima">　describe（'getArtist',（）=&gt; {</p><p class="daima">　　it（'retrieves using the artist ID',</p><p class="daima">　　　inject（[SpotifyService, MockBackend], fakeAsync（（svc, backend）=&gt; {</p><p class="daima">　　　　var res;</p><p class="daima">　　　　expectURL（backend, 'https：//api.spotify.com/v1/artists/ARTIST_ID'）;</p><p class="daima">　　　　svc.getArtist（'ARTIST_ID'）.subscribe（（_res）=&gt; {</p><p class="daima">　　　　　res = _res;</p><p class="daima">　　　　}）;</p><p class="daima">　　　　tick（）;</p><p class="daima">　　　　expect（res.name）.toBe（'felipe'）;</p><p class="daima">　　　}））</p><p class="daima">　　）;</p><p class="daima">　}）;</p><p class="daima">　describe（'getAlbum',（）=&gt; {</p><p class="daima">　　it（'retrieves using the album ID',</p><p class="daima">　　　inject（[SpotifyService, MockBackend], fakeAsync（（svc, backend）=&gt; {</p><p class="daima">　　　　var res;</p><p class="daima">　　　　expectURL（backend, 'https：//api.spotify.com/v1/albums/ALBUM_ID'）;</p><p class="daima">　　　　svc.getAlbum（'ALBUM_ID'）.subscribe（（_res）=&gt; {</p><p class="daima">　　　　　res = _res;</p><p class="daima">　　　　}）;</p><p class="daima">　　　　tick（）;</p><p class="daima">　　　　expect（res.name）.toBe（'felipe'）;</p><p class="daima">　　　}））</p><p class="daima">　　）;</p><p class="daima">　}）;</p><p class="content">searchTrack方法稍有不同：它不直接调用query，而是使用search方法替代。</p><p class="reference"><b>code/routes/music/app/ts/services/SpotifyService.ts</b></p><p class="daima">　searchTrack（query：string）：Observable&lt;any[]&gt; {</p><p class="daima">　　return this.search（query, 'track'）;</p><p class="daima">　}</p><p class="content">search接着调用query，将/search作为第一个参数并将一个包含q=&lt;query&gt;和type=track的数组作为第二个参数。</p><p class="reference"><b>code/routes/music/app/ts/services/SpotifyService.ts</b></p><p class="daima">　search（query：string, type：string）：Observable&lt;any[]&gt; {</p><p class="daima">　　return this.query（`/search`, [</p><p class="daima">　　　`q=${query}`,</p><p class="daima">　　　`type=${type}`</p><p class="daima">　　]）;</p><p class="daima">　}</p><p class="content">最后，query将参数转换成带有QueryString的<b>URL路径</b>。我们期待调用的URL是以/search?q=&amp;type=track结尾的。</p><p class="content">综合所学知识，为searchTrack方法编写测试。</p><p class="reference"><b>code/routes/music/test/services/SpotifyService.spec.ts</b></p><p class="daima">　describe（'searchTrack',（）=&gt; {</p><p class="daima">　　it（'searches type and term',</p><p class="daima">　　　inject（[SpotifyService, MockBackend], fakeAsync（（svc, backend）=&gt; {</p><p class="daima">　　　　var res;</p><p class="daima">　　　　expectURL（backend, 'https：//api.spotify.com/v1/search?q=TERM&amp;type=track'\</p><p class="daima">）;</p><p class="daima">　　　　svc.searchTrack（"TERM"）.subscribe（（_res）=&gt; {</p><p class="daima">　　　　　res = _res;</p><p class="daima">　　　　}）;</p><p class="daima">　　　　tick（）;</p><p class="daima">　　　　expect（res.name）.toBe（'felipe'）;</p><p class="daima">　　　}））</p><p class="daima">　　）;</p><p class="daima">　}）;</p><p class="content">这个测试与之前写过的测试异曲同工。下面一起回顾这个测试的内容：</p><p class="content">●植入HTTP生命周期，在HTTP连接初始化时添加回调；</p><p class="content">●为当前连接设置预期URL，包含查询类型和搜索关键字；</p><p class="content">●调用测试方法searchTrack；</p><p class="content">●通知Angular完成所有等待的异步调用；</p><p class="content">●断言预期响应结果。</p><p class="content">简而言之，测试服务类时要做的是：</p><p class="content">（1）使用伪装或模拟来隔离全部依赖；</p><p class="content">（2）在异步调用的情况下，使用fakeAsync和tick确保它们的完成；</p><p class="content">（3）调用要测试的服务类；</p><p class="content">（4）断言方法返回值与预期值匹配。</p><p class="content">下面把注意力转向那些消费服务的类：组件。</p><p class="sectionTitle biaoti3"><a id="sec008"></a>15.8 测试组件间的路由</p><p class="content">测试组件时，可以使用下列两种策略之一：</p><p class="content">（1）编写测试从外部与组件进行交互，传递属性值，检验标签生成结果；</p><p class="content">（2）测试各个组件方法及其输出结果。</p><p class="content">这两种测试策略分别称为<b>黑盒测试</b>和<b>白盒测试</b>。本节将演示如何混合使用它们。</p><p class="content">首先为相对简单的一个组件ArtistComponent类编写测试。第一部分测试将测试组件的内部结构，所以它属于<b>白盒测试</b>。</p><p class="content">在开始之前，先回顾一下ArtistComponent的内容：</p><p class="content">在类的构造函数上，首先从routeParams集合中读取id。</p><p class="reference"><b>code/routes/music/app/ts/components/ArtistComponent.ts</b></p><p class="daima">　constructor（private route：ActivatedRoute, private spotify：SpotifyService,</p><p class="daima">　　　　　　　private location：Location）{</p><p class="daima">　　route.params.subscribe（params =&gt; { this.id = params['id']; }）;</p><p class="daima">　}</p><p class="content">很快，我们遇到了第一个绊脚石：在没有运行状态路由器的情况下，如何获取当前路由的ID？</p><p class="listTitle1 biaoti8">15.8.1 为测试创建路由器</p><p class="content">在Angular中写测试时，我们手动配置了大量注入的类。路由（和测试组件）也包含大量需要注入的依赖。尽管如此，一旦配置好了，它就很少变更而且简单易用。</p><p class="content">写测试时，使用beforeEach和TestBed.configureTestingModule设置可注入的依赖是很方便的。在测试ArtistComponent的时候，将定义一个函数来创建和配置这个测试的路由。</p><p class="reference"><b>code/routes/music/test/components/ArtistComponent.spec.ts</b></p><p class="daima">describe（'ArtistComponent',（）=&gt; {</p><p class="daima">　beforeEach（（）=&gt; {</p><p class="daima">　　configureMusicTests（）;</p><p class="daima">　}）;</p><p class="content">在辅助类文件MusicTestHelpers.ts中定义方法configureMusicTests。一起来看看。</p><p class="content">这仅仅是configureMusicTests的实现代码。不用担心，下面将逐一解释。</p><p class="reference"><b>code/routes/music/test/MusicTestHelpers.ts</b></p><p class="daima">export function configureMusicTests（）{</p><p class="daima">　const mockSpotifyService：MockSpotifyService = new MockSpotifyService（）;</p><p class="daima">　TestBed.configureTestingModule（{</p><p class="daima">　　imports：[</p><p class="daima">　　　{ // TODO RouterTestingModule.withRoutes coming soon</p><p class="daima">　　　　ngModule：RouterTestingModule,</p><p class="daima">　　　　providers：[provideRoutes（routerConfig）]</p><p class="daima">　　　},</p><p class="daima">　　　TestModule</p><p class="daima">　　],</p><p class="daima">　　providers：[</p><p class="daima">　　　mockSpotifyService.getProviders（）,</p><p class="daima">　　　{</p><p class="daima">　　　　provide：ActivatedRoute,</p><p class="daima">　　　　useFactory：（r：Router）=&gt; r.routerState.root, deps：[ Router ]</p><p class="daima">　　　}</p><p class="daima">　　]</p><p class="daima">　}）;</p><p class="daima">}</p><p class="content">首先创建一个MockSpotifyService实例，用来模拟真实的SpotifyService实现。</p><p class="content">接下来，使用一个名为TestBed的类，并调用其方法configureTestingModule。TestBed是Angular内置的一个辅助类库，帮助我们简化测试。</p><p class="content">本例中，TestBed.configureTestingModule的作用是为测试配置NgModule。你可以看到我们提供了一个NgModule配置作为参数，它包含：</p><p class="content">●imports</p><p class="content">●providers</p><p class="content">在imports中，导入：</p><p class="content">●RouterTestingModule，并用routerConfig进行配置——这样能够为测试配置路由器；</p><p class="content">●TestModule，这个NgModule声明了所有将要测试的组件（具体细节参见MusicTestHelpers.ts）。</p><p class="content">在providers中，提供了：</p><p class="content">●MockSpotifyService（通过mockSpotifyService.getProviders（））</p><p class="content">●ActivatedRoute</p><p class="content">我们以Router为入口，进一步学习。</p><p class="content">●<b>Router</b></p><p class="content">至今尚未提及的是测试时要用到哪些路由。对此有多种方法实现，首先看一下我们要用的方式。</p><p class="reference"><b>code/routes/music/test/MusicTestHelpers.ts</b></p><p class="daima">@Component（{</p><p class="daima">　selector：'blank-cmp',</p><p class="daima">　template：``</p><p class="daima">}）</p><p class="daima">export class BlankCmp {</p><p class="daima">}</p><p class="daima">@Component（{</p><p class="daima">　selector：'root-cmp',</p><p class="daima">　template：`&lt;router-outlet&gt;&lt;/router-outlet&gt;`</p><p class="daima">}）</p><p class="daima">export class RootCmp {</p><p class="daima">}</p><p class="daima">export const routerConfig：Routes = [</p><p class="daima">　{ path：'', component：BlankCmp },</p><p class="daima">　{ path：'search', component：SearchComponent },</p><p class="daima">　{ path：'artists/：id', component：ArtistComponent },</p><p class="daima">　{ path：'tracks/：id', component：TrackComponent },</p><p class="daima">　{ path：'albums/：id', component：AlbumComponent }</p><p class="daima">];</p><p class="content">这里并不（像真实路由器配置的那样）跳转到一个空的URL，而是使用一个BlankCmp替代。</p><p class="content">当然，如果你坚持像顶层应用那样使用RouterConfig，那么要先在其他地方使用export导出，并在此处使用import导入。</p><p class="content">如果遇到更复杂的场景，必须针对多种不同的路由配置进行测试，那么可以在musicTestProviders函数中接收一个参数，从而每次运行测试都使用一个新的路由器配置。</p><p class="content">面临太多的选择，你必须挑选一种最适合自己团队的方式。在路由是相对静态的并且一个配置可以服务于所有测试的情况下，这个配置相当棒。</p><p class="content">现在所有依赖都已经解决，可以通过new Router创建一个新的路由器，并调用其r.initialNavigation（）方法。</p><p class="content">●<b>ActivatedRoute</b></p><p class="content">ActivatedRoute服务跟踪“当前路由”。它需要把Router作为依赖，并把它加入到deps来进行注入。</p><p class="content">●<b>MockSpotifyService</b></p><p class="content">之前通过模拟HTTP库测试了SpotifyService。这里我们将会模拟<b>整个服务类</b>。一起来看看如何模拟这个类，或者说任何服务。</p><p class="listTitle1 biaoti8">15.8.2 模拟依赖</p><p class="content">在music/test目录下，找到mocks/spotify.ts文件，内容如下。</p><p class="reference"><b>code/routes/music/test/mocks/spotify.ts</b></p><p class="daima">import {SpyObject} from './helper';</p><p class="daima">import {SpotifyService} from '../../app/ts/services/SpotifyService';</p><p class="daima">export class MockSpotifyService extends SpyObject {</p><p class="daima">　getAlbumSpy;</p><p class="daima">　getArtistSpy;</p><p class="daima">　getTrackSpy;</p><p class="daima">　searchTrackSpy;</p><p class="daima">　mockObservable;</p><p class="daima">　fakeResponse;</p><p class="content">这里声明MockSpotifyService服务类，它是真实SpotifyService的一个模拟版本。这些实例变量会被作为<b>探子</b>（spy）使用。</p><p class="listTitle1 biaoti8">15.8.3 探子</p><p class="content"><b>探子</b>是一种比较特别的模拟对象，有两个好处：</p><p class="content">（1）可以模拟返回值；</p><p class="content">（2）可以计算方法调用次数和调用的参数值。</p><p class="content">要在Angular测试中使用探子，可以使用一个内部类SpyObject实现（用于Angular内部测试）。</p><p class="content">正如我们的代码所示，你可以即时创建一个新SpyObject或者让模拟类继承SpyObject。</p><p class="content">继承或直接使用这个类的好处在于，它提供一个spy方法。spy方法允许你覆盖某个方法并强制返回值（以及监控，确保方法被调用）。下面的代码对类构造函数使用spy。</p><p class="reference"><b>code/routes/music/test/mocks/spotify.ts</b></p><p class="daima">　constructor（）{</p><p class="daima">　　super（SpotifyService）;</p><p class="daima">　　this.fakeResponse = null;</p><p class="daima">　　this.getAlbumSpy = this.spy（'getAlbum'）.andReturn（this）;</p><p class="daima">　　this.getArtistSpy = this.spy（'getArtist'）.andReturn（this）;</p><p class="daima">　　this.getTrackSpy = this.spy（'getTrack'）.andReturn（this）;</p><p class="daima">　　this.searchTrackSpy = this.spy（'searchTrack'）.andReturn（this）;</p><p class="daima">　}</p><p class="content">构造函数的第一行调用了SpyObject构造函数，传递要模拟的特定类。调用super（……）是可选的，但模拟时类会继承所有特定类的方法，因此你只需要覆盖要测试的方法。</p><p class="reference"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t20827/10/215994487/12231/293f6125/5b03d541Ndff69dec.jpg" class="inline1" alt="" href="./image/a-3.jpg" />　如果你想知道SpyObject是如何实现的，请查看angular/angular项目下的文件/modules/angular2/src/testing/testing_internal.ts（https：//github.com/angular/angular/blob/b0cebd-ba6b651e9e7eb5bf801ea42dc7c4a7f25/modules/angular2/src/testing/testing_internal.ts#L205）。</p><p class="content">调用super之后，将fakeResponse的值初始化为null，我们稍后会用到它。</p><p class="content">接下来用探子替换特定类的方法。编写测试时使用一个引用更容易设置预期值和模拟响应结果。</p><p class="content">在ArtistComponent中使用SpotifyService时，真实的getArtist方法返回一个可观察对象。在组件中调用的方法是subscribe方法。</p><p class="reference"><b>code/routes/music/app/ts/components/ArtistComponent.ts</b></p><p class="daima">　ngOnInit（）：void {</p><p class="daima">　　this.spotify</p><p class="daima">　　　.getArtist（this.id）</p><p class="daima">　　　.subscribe（（res：any）=&gt; this.renderArtist（res））;</p><p class="daima">　}</p><p class="content">然而在模拟类中，我们会采取一个小技巧：getArtist并不返回可观察对象，而是返回this，也就是MockSpotifyService自身。这就意味着上面this.spotify.getArtist（this.id）的返回值是MockSpotifyService。</p><p class="content">不过这样有一个问题：ArtistComponent将会调用可观察对象的subscribe方法。考虑到这一点，可以在MockSpotifyService中定义一个subscribe方法。</p><p class="reference"><b>code/routes/music/test/mocks/spotify.ts</b></p><p class="daima">　subscribe（callback）{</p><p class="daima">　　callback（this.fakeResponse）;</p><p class="daima">　}</p><p class="content">现在在模拟对象上调用subscribe方法，会立即调用这个回调函数，异步方法会同步执行。</p><p class="content">另外注意，我们使用this.fakeResponse来调用这个回调函数。它将我们引向另一个方法。</p><p class="reference"><b>code/routes/music/test/mocks/spotify.ts</b></p><p class="daima">　setResponse（json：any）：void {</p><p class="daima">　　this.fakeResponse = json;</p><p class="daima">　}</p><p class="content">这个方法并没有替换特定服务的任何部件，取而代之的是使用一个辅助方法，允许测试代码设置既定的响应结果（可能来源于特定的服务），并利用它模拟不同的响应。</p><p class="reference"><b>code/routes/music/test/mocks/spotify.ts</b></p><p class="daima">　getProviders（）：Array&lt;any&gt; {</p><p class="daima">　　return [{ provide：SpotifyService, useValue：this }];</p><p class="daima">　}</p><p class="content">最后一个方法是辅助方法，用在TestBed.configureTestingModule的providers参数上。它和稍后回过头来写组件测试时看到的类似。</p><p class="content">下面是MockSpotifyService的完整代码。</p><p class="reference"><b>code/routes/music/test/mocks/spotify.ts</b></p><p class="daima">import {SpyObject} from './helper';</p><p class="daima">import {SpotifyService} from '../../app/ts/services/SpotifyService';</p><p class="daima">export class MockSpotifyService extends SpyObject {</p><p class="daima">　getAlbumSpy;</p><p class="daima">　getArtistSpy;</p><p class="daima">　getTrackSpy;</p><p class="daima">　searchTrackSpy;</p><p class="daima">　mockObservable;</p><p class="daima">　fakeResponse;</p><p class="daima">　constructor（）{</p><p class="daima">　　super（SpotifyService）;</p><p class="daima">　　this.fakeResponse = null;</p><p class="daima">　　this.getAlbumSpy = this.spy（'getAlbum'）.andReturn（this）;</p><p class="daima">　　this.getArtistSpy = this.spy（'getArtist'）.andReturn（this）;</p><p class="daima">　　this.getTrackSpy = this.spy（'getTrack'）.andReturn（this）;</p><p class="daima">　　this.searchTrackSpy = this.spy（'searchTrack'）.andReturn（this）;</p><p class="daima">　}</p><p class="daima">　subscribe（callback）{</p><p class="daima">　　callback（this.fakeResponse）;</p><p class="daima">　}</p><p class="daima">　setResponse（json：any）：void {</p><p class="daima">　　this.fakeResponse = json;</p><p class="daima">　}</p><p class="daima">　getProviders（）：Array&lt;any&gt; {</p><p class="daima">　　return [{ provide：SpotifyService, useValue：this }];</p><p class="daima">　}</p><p class="daima">}</p><p class="sectionTitle biaoti3"><a id="sec009"></a>15.9 回到测试代码</p><p class="content">万事俱备，只欠东风。现在可以为ArtistComponent编写测试代码了。</p><p class="content">首先是导入语句。</p><p class="reference"><b>code/routes/music/test/components/ArtistComponent.spec.ts</b></p><p class="daima">import {</p><p class="daima">　inject,</p><p class="daima">　fakeAsync,</p><p class="daima">} from '@angular/core/testing';</p><p class="daima">import { Router } from '@angular/router';</p><p class="daima">import { Location } from '@angular/common';</p><p class="daima">import { MockSpotifyService } from '../mocks/spotify';</p><p class="daima">import { SpotifyService } from '../../app/ts/services/SpotifyService';</p><p class="daima">import {</p><p class="daima">　advance,</p><p class="daima">　createRoot,</p><p class="daima">　RootCmp,</p><p class="daima">　configureMusicTests</p><p class="daima">} from '../MusicTestHelpers';</p><p class="content">接下来使用configureMusicTests描述测试，确保所有测试用例都可以访问musicTestProviders。</p><p class="reference"><b>code/routes/music/test/components/ArtistComponent.spec.ts</b></p><p class="daima">describe（'ArtistComponent',（）=&gt; {</p><p class="daima">　beforeEach（（）=&gt; {</p><p class="daima">　　configureMusicTests（）;</p><p class="daima">　}）;</p><p class="content">然后写一个测试来验证组件初始化的细节。首先，回顾一下ArtistComponent的初始化过程。</p><p class="reference"><b>code/routes/music/app/ts/components/ArtistComponent.ts</b></p><p class="daima">export class ArtistComponent implements OnInit {</p><p class="daima">　id：string;</p><p class="daima">　artist：Object;</p><p class="daima">　constructor（private route：ActivatedRoute, private spotify：SpotifyService,</p><p class="daima">　　　　　　　private location：Location）{</p><p class="daima">　　route.params.subscribe（params =&gt; { this.id = params['id']; }）;</p><p class="daima">　}</p><p class="daima">　ngOnInit（）：void {</p><p class="daima">　　this.spotify</p><p class="daima">　　　.getArtist（this.id）</p><p class="daima">　　　.subscribe（（res：any）=&gt; this.renderArtist（res））;</p><p class="daima">　}</p><p class="content">请记住，创建组件时，使用route.params接收当时路由的id参数，并将它存储在类的id属性中。</p><p class="content">当组件初始化时，ngOnInit方法被Angular触发（因为此组件实现了OnInit接口）。然后针对接收到的id使用SpotifyService读取相应的艺术家。当获取艺术家数据后，调用renderArtist，传递艺术家数据。</p><p class="content">这里一个重要的理念就是使用依赖注入来获取SpotifyService，但是要记得，我们之前已经<b>创建了一个</b>MockSpotifyService。</p><p class="content">为了测试这一行为，执行以下步骤：</p><p class="content">（1）使用路由导向到ArtistComponent，组件会进行初始化；</p><p class="content">（2）验证MockSpotifyService在ArtistComponent中已经被注入，根据相应的id读取艺术家数据。</p><p class="content">下面是完整的测试代码。</p><p class="reference"><b>code/routes/music/test/components/ArtistComponent.spec.ts</b></p><p class="daima">　describe（'initialization',（）=&gt; {</p><p class="daima">　　it（'retrieves the artist', fakeAsync（</p><p class="daima">　　　inject（[Router, SpotifyService],</p><p class="daima">　　　　　　（router：Router,</p><p class="daima">　　　　　　　mockSpotifyService：MockSpotifyService）=&gt; {</p><p class="daima">　　　　const fixture = createRoot（router, RootCmp）;</p><p class="daima">　　　　router.navigateByUrl（'/artists/2'）;</p><p class="daima">　　　　advance（fixture）;</p><p class="daima">　　　　expect（mockSpotifyService.getArtistSpy）.toHaveBeenCalledWith（'2'）;</p><p class="daima">　　　}）））;</p><p class="daima">　}）;</p><p class="content">接下来一步步进行解释。</p><p class="listTitle1 biaoti8">15.9.1 fakeAsync和advance</p><p class="content">首先用fakeAsync包裹测试。有了fakeAsync，我们就能够在状态检测和异步操作发生时进行更多的控制，并且不需要深入其内部细节。这样做的结果是，我们在测试中做了变更，必须显式地通知组件检测变更结果。</p><p class="content">一般来说，开发程序时不必担心这个问题，这是Zones应该做的事。但在整个测试过程中，我们可以更细致地对状态变化的检测进行操作。</p><p class="content">向下跳过几行，可以看到调用了MusicTestHelpers中的advance函数。一起看看这个函数。</p><p class="reference"><b>code/routes/music/test/MusicTestHelpers.ts</b></p><p class="daima">export function advance（fixture：ComponentFixture&lt;any&gt;）：void {</p><p class="daima">　tick（）;</p><p class="daima">　fixture.detectChanges（）;</p><p class="daima">}</p><p class="content">advance函数做了两件事：</p><p class="content">（1）通知组件检测状态变更；</p><p class="content">（2）调用tick（）。</p><p class="content">使用fakeAsync时，计时器是同步的。我们使用tick（）来模拟异步流逝的时间。</p><p class="content">实际上，在我们的测试中，任何需要Angular大显身手的时候都可以调用advance函数。例如，如果要导向到新的路由，更新一个表单元素，发出一个HTTP请求等，我们都可以调用advance函数给Angular制造机会大显神通。</p><p class="listTitle1 biaoti8">15.9.2 inject</p><p class="content">在测试中需要添加一些依赖。使用inject可以做到这一点。inject接收两个参数：</p><p class="content">（1）一个等待注入的<b>令牌</b>数组</p><p class="content">（2）一个提供了注入的函数</p><p class="content">inject会使用哪些类？提供者通过TestBed.configureTestingModule的providers来定义。</p><p class="content">注意，这里要注入：</p><p class="content">（1）Router</p><p class="content">（2）SpotifyService</p><p class="content">要注入的Router类就是上面musicTestProviders中配置的Router。</p><p class="content">对于SpotifyService，注意请求注入SpotifyService时，得到的是MockSpotifyService。看起来有点晦涩，但是基于目前为止的讨论你应该可以理解。</p><p class="listTitle1 biaoti8">15.9.3 测试ArtistComponent组件初始化</p><p class="content">一起回顾一下测试代码的内容。</p><p class="reference"><b>code/routes/music/test/components/ArtistComponent.spec.ts</b></p><p class="daima">　　　const fixture = createRoot（router, RootCmp）;</p><p class="daima">　　　router.navigateByUrl（'/artists/2'）;</p><p class="daima">　　　advance（fixture）;</p><p class="daima">　　　expect（mockSpotifyService.getArtistSpy）.toHaveBeenCalledWith（'2'）;</p><p class="content">我们使用createRoot创建一个RootCmp实例。一起看看createRoot辅助函数。</p><p class="reference"><b>code/routes/music/test/MusicTestHelpers.ts</b></p><p class="daima">export function createRoot（router：Router,</p><p class="daima">　　　　　　　　　　　　　 componentType：any）：ComponentFixture&lt;any&gt; {</p><p class="daima">　const f = TestBed.createComponent（componentType）;</p><p class="daima">　advance（f）;</p><p class="daima">　（&lt;any&gt;router）.initialNavigation（）;</p><p class="daima">　advance（f）;</p><p class="daima">　return f;</p><p class="daima">}</p><p class="content">注意，这时调用createRoot可以：</p><p class="content">（1）创建一个根组件实例；</p><p class="content">（2）对其进行advance处理；</p><p class="content">（3）通知路由器设置initialNavigation；</p><p class="content">（4）再次进行advance处理；</p><p class="content">（5）返回全新的根组件。</p><p class="content">测试依赖路由器的组件时需要不少准备工作，有这个辅助函数可以方便不少。</p><p class="content">注意我们再次使用了TestBed类库来调用TestBed.createComponent方法。这个方法创建了一个相应类型的组件。</p><p class="center"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t18559/108/2587195628/17417/8ceb743d/5b03d540N559a889f.jpg" class="enlarge" alt="" href="./image/a-9.jpg" /></p><p class="content">RootCmp是我们在MusicTestHelpers中创建的一个空组件。其实完全没必要为根组件创建一个空组件。这里这样做是因为能够或多或少在隔离环境中测试子组件（ArtistComponent）。这样一来，就不必担心对上层应用组件的影响了。</p><p class="content">但是，也许你<b>想要</b>确保子组件在上下文环境中正确运行。在这种情况下，你可能想使用应用程序常规的父组件，而非RootCmp。</p><p class="content">接下来探讨使用router转向URL /artists/2以及advance。当我们定位到该URL时，ArtistComponent应该会进行初始化，因此可以断定调用SpotifyService的getArtist方法时返回了正确的值。</p><p class="listTitle1 biaoti8">15.9.4 测试ArtistComponent方法</p><p class="content">回想一下，ArtistComponent中有一个href调用了back（）方法。</p><p class="reference"><b>code/routes/music/app/ts/components/ArtistComponent.ts</b></p><p class="daima">　back（）：void {</p><p class="daima">　　this.location.back（）;</p><p class="daima">　}</p><p class="content">下面来测试，当调用back方法时，路由器会将用户重定向回之前的位置。</p><p class="content">当前位置状态由Location服务控制。当需要将用户返回原来的位置时，我们使用Location的back方法。</p><p class="content">这里演示如何测试back方法。</p><p class="reference"><b>code/routes/music/test/components/ArtistComponent.spec.ts</b></p><p class="daima">　describe（'back',（）=&gt; {</p><p class="daima">　　it（'returns to the previous location', fakeAsync（</p><p class="daima">　　　inject（[Router, Location],</p><p class="daima">　　　　　　（router：Router, location：Location）=&gt; {</p><p class="daima">　　　　const fixture = createRoot（router, RootCmp）;</p><p class="daima">　　　　expect（location.path（））.toEqual（'/'）;</p><p class="daima">　　　　router.navigateByUrl（'/artists/2'）;</p><p class="daima">　　　　advance（fixture）;</p><p class="daima">　　　　expect（location.path（））.toEqual（'/artists/2'）;</p><p class="daima">　　　　const artist = fixture.debugElement.children[1].componentInstance;</p><p class="daima">　　　　artist.back（）;</p><p class="daima">　　　　advance（fixture）;</p><p class="daima">　　　　expect（location.path（））.toEqual（'/'）;</p><p class="daima">　　　}）））;</p><p class="daima">　}）;</p><p class="content">初始结构与之类似：注入依赖并创建一个新的组件。</p><p class="content">加入一个新的expect语句，断定location.path（）与预期结果一致。</p><p class="content">这里提供一种新思路：当访问ArtistComponent的方法时，通过fixture.debugElement.children[1].componentInstance这一行得到ArtistComponent实例的一个引用。</p><p class="content">有了组件实例，就可以直接调用其方法了，如back（）。</p><p class="content">调用了back方法后，进行advance处理，然后验证location.path（）是否与预期一致。</p><p class="listTitle1 biaoti8">15.9.5 测试ArtistComponent DOM模板值</p><p class="content">最后要测试ArtistComponent的一部分就是生成艺术家模板。</p><p class="reference"><b>code/routes/music/app/ts/components/ArtistComponent.ts</b></p><p class="daima">　template：`</p><p class="daima">　&lt;div *ngIf="artist"&gt;</p><p class="daima">　　&lt;h1&gt;{{ artist.name }}&lt;/h1&gt;</p><p class="daima">　　&lt;p&gt;</p><p class="daima">　　　&lt;img src="{{ artist.images[0].url }}"&gt;</p><p class="daima">　　&lt;/p&gt;</p><p class="daima">　　&lt;p&gt;&lt;a href（click）="back（）"&gt;Back&lt;/a&gt;&lt;/p&gt;</p><p class="daima">　&lt;/div&gt;</p><p class="daima">　`</p><p class="content">记住，实例变量artist是SpotifyService类getArtist方法的调用结果。既然用MockSpotifyService模拟SpotifyService，那么模板中使用的数据无论如何都应该是mockSpotifyService的返回结果。下面一起看看如何实现。</p><p class="reference"><b>code/routes/music/test/components/ArtistComponent.spec.ts</b></p><p class="daima">　describe（'renderArtist',（）=&gt; {</p><p class="daima">　　it（'renders album info', fakeAsync（</p><p class="daima">　　　inject（[Router, SpotifyService],</p><p class="daima">　　　　　　（router：Router,</p><p class="daima">　　　　　　　mockSpotifyService：MockSpotifyService）=&gt; {</p><p class="daima">　　　　const fixture = createRoot（router, RootCmp）;</p><p class="daima">　　　　let artist = {name：'ARTIST NAME', images：[<span class="underline">rl：'IMAGE_1'</span>]};</p><p class="daima">　　　　mockSpotifyService.setResponse（artist）;</p><p class="daima">　　　　router.navigateByUrl（'/artists/2'）;</p><p class="daima">　　　　advance（fixture）;</p><p class="daima">　　　　const compiled = fixture.debugElement.nativeElement;</p><p class="daima">　　　　expect（compiled.querySelector（'h1'）.innerHTML）.toContain（'ARTIST NAME'）;</p><p class="daima">　　　　expect（compiled.querySelector（'img'）.src）.toContain（'IMAGE_1'）;</p><p class="daima">　　　}）））;</p><p class="daima">　}）;</p><p class="content">这里比较陌生的是通过mockSpotifyService的setResponse方法手动设置返回结果。</p><p class="content">artist变量是一个<b>测试工具夹</b>，代表调用artists终端即使用GET方法请求https：//api.spotify.com/v1/artists/{id}时从Spotify API返回的结果。</p><p class="content">真实的JSON数据看起来如图15-1所示。</p><p class="center"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t21064/40/198495146/111795/c9de0775/5b03d541Na7b73887.jpg" class="enlarge" alt="" href="./image/a-144.jpg" /></p><p class="descrip"><b>图15-1　Spotify中用来获取艺术家的服务端点</b></p><p class="content">但是对于这个测试，我们仅需要name和images属性。</p><p class="content">调用setResponse时，响应会作用于后面所有调用服务方法的下一轮调用过程。在本例中，我们要的结果是getArtist方法返回此响应。</p><p class="content">接下来，通过路由器定位并进行advance处理。现在视图已经生成，可以使用组件视图的DOM表现形式检测是否已经正确地生成了艺术家。</p><p class="content">fixture.debugElement.nativeElement一行中读取DebugElement的nativeElement属性可以做到这一点。</p><p class="content">在断言语句中，我们期望H1标签中包含艺术家的名字，在本例中是字符串ARTIST NAME（源自上面的artist工具夹）。</p><p class="content">为了检查这些条件，我们用到了NativeElement的querySelector方法。此方法会返回与CSS选择器匹配的第一个元素。</p><p class="content">对于H1，我们检测其文本内容确实是ARTIST NAME，而图片的src属性值为IMAGE 1。</p><p class="content">至此，我们已经完成了对ArtistComponent组件的测试。</p><p class="sectionTitle biaoti3"><a id="sec010"></a>15.10 测试表单</p><p class="content">为了演示为表单编写测试，我们使用在第5章中创建的DemoFormNgModel组件。这个例子很不错，因为它用到了Angular表单的一些特性：</p><p class="content">●使用FormBuilder</p><p class="content">●包含表单验证</p><p class="content">●处理事件</p><p class="content">下面是这个类的完整代码。</p><p class="reference"><b>code/forms/app/forms/demo_form_with_events.ts</b></p><p class="daima">import { Component } from '@angular/core';</p><p class="daima">import {</p><p class="daima">　FormBuilder,</p><p class="daima">　FormGroup,</p><p class="daima">　Validators,</p><p class="daima">　AbstractControl</p><p class="daima">} from '@angular/forms';</p><p class="daima">@Component（{</p><p class="daima">　selector：'demo-form-with-events',</p><p class="daima">　template：`</p><p class="daima">　&lt;div class="ui raised segment"&gt;</p><p class="daima">　　&lt;h2 class="ui header"&gt;Demo Form：with events&lt;/h2&gt;</p><p class="daima">　　&lt;form [formGroup]="myForm"</p><p class="daima">　　　　　（ngSubmit）="onSubmit（myForm.value）"</p><p class="daima">　　　　　class="ui form"&gt;</p><p class="daima">　　　&lt;div class="field"</p><p class="daima">　　　　　[class.error]="！sku.valid &amp;&amp; sku.touched"&gt;</p><p class="daima">　　　　&lt;label for="skuInput"&gt;SKU&lt;/label&gt;</p><p class="daima">　　　　&lt;input type="text"</p><p class="daima">　　　　　　　 class="form-control"</p><p class="daima">　　　　　　　 id="skuInput"</p><p class="daima">　　　　　　　 placeholder="SKU"</p><p class="daima">　　　　　　　 [formControl]="sku"&gt;</p><p class="daima">　　　　&lt;div *ngIf="！sku.valid"</p><p class="daima">　　　　　class="ui error message"&gt;SKU is invalid&lt;/div&gt;</p><p class="daima">　　　　&lt;div *ngIf="sku.hasError（'required'）"</p><p class="daima">　　　　　class="ui error message"&gt;SKU is required&lt;/div&gt;</p><p class="daima">　　　&lt;/div&gt;</p><p class="daima">　　　&lt;div *ngIf="！myForm.valid"</p><p class="daima">　　　　class="ui error message"&gt;Form is invalid&lt;/div&gt;</p><p class="daima">　　　&lt;button type="submit" class="ui button"&gt;Submit&lt;/button&gt;</p><p class="daima">　　&lt;/form&gt;</p><p class="daima">　&lt;/div&gt;</p><p class="daima">　`</p><p class="daima">}）</p><p class="daima">export class DemoFormWithEvents {</p><p class="daima">　myForm：FormGroup;</p><p class="daima">　sku：AbstractControl;</p><p class="daima">　constructor（fb：FormBuilder）{</p><p class="daima">　　this.myForm = fb.group（{</p><p class="daima">　　　'sku'：['', Validators.required]</p><p class="daima">　　}）;</p><p class="daima">　　this.sku = this.myForm.controls['sku'];</p><p class="daima">　　this.sku.valueChanges.subscribe（</p><p class="daima">　　　（value：string）=&gt; {</p><p class="daima">　　　　console.log（'sku changed to：', value）;</p><p class="daima">　　　}</p><p class="daima">　　）;</p><p class="daima">　　this.myForm.valueChanges.subscribe（</p><p class="daima">　　　（form：any）=&gt; {</p><p class="daima">　　　　console.log（'form changed to：', form）;</p><p class="daima">　　　}</p><p class="daima">　　）;</p><p class="daima">　}</p><p class="daima">　onSubmit（form：any）：void {</p><p class="daima">　　console.log（'you submitted value：', form.sku）;</p><p class="daima">　}</p><p class="daima">}</p><p class="content">回顾一下，这段代码包含以下行为：</p><p class="content">●当没有值填充SKU字段时，会显示两条验证错误信息，分别是SKU is invalid和SKU is required；</p><p class="content">●当SKU字段的值发生改变时，在控制台打印一条日志信息；</p><p class="content">●当表单发生改变时，也在控制台打印一条日志信息；</p><p class="content">●当提交表单时，在控制台打印最后一条日志信息。</p><p class="content">很显然，我们用到了一个外部依赖，即控制台。正如我们之前学到的那样，必须用一些技巧来模拟所有外部依赖。</p><p class="listTitle1 biaoti8">15.10.1 创建一个ConsoleSpy</p><p class="content">这次不用SpyObject来创建伪对象。既然我们所有使用console的场景都是调用log方法，可以让事情更简单一些。</p><p class="content">用我们能够掌控的ConsoleSpy替换原来依赖于window.console对象的console实例。</p><p class="reference"><b>code/forms/test/util.ts</b></p><p class="daima">export class ConsoleSpy {</p><p class="daima">　public logs：string[] = [];</p><p class="daima">　log（……args）{</p><p class="daima">　　this.logs.push（args.join（' '））;</p><p class="daima">　}</p><p class="daima">　warn（……args）{</p><p class="daima">　　this.log（……args）;</p><p class="daima">　}</p><p class="daima">}</p><p class="content">ConsoleSpy会接收所有日志记录，简单地转换成字符串，并存储在其内部一个日志记录字符串列表中。</p><p class="center"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t20827/10/215994487/12231/293f6125/5b03d541Ndff69dec.jpg" class="enlarge" alt="" href="./image/a-3.jpg" /></p><p class="content">在我们的console.log版本中，为了接收可变参数，我们使用ES6和TypeScript的Rest参数<sup><a class="mz-footnote-link" href="#footnote4"><img alt="" src="https://img30.360buyimg.com/ebookadmin/jfs/t21616/337/214486965/1481/90828627/5b03d540N24da8434.png" href="./image/icon_note.png" /></a></sup>。</p><p class="content">这个操作符由省略号表示，如我们的函数参数……theArgs。简单概括，使用它表示我们将接收从点号起所有剩余的参数。例如（a, b, ……theArgs）调用了func（1, 2, 3, 4, 5），那么a应该是1，b应该是2，而theArgs应该包含数组[3, 4, 5]。</p><p class="content">如果你安装了最新的Node.js<sup><a class="mz-footnote-link" href="#footnote5"><img alt="" src="https://img30.360buyimg.com/ebookadmin/jfs/t21616/337/214486965/1481/90828627/5b03d540N24da8434.png" href="./image/icon_note.png" /></a></sup>，可以自己尝试一下：</p><p class="daima">　 $ node ——harmony</p><p class="daima">　　var test =（a, b, ……theArgs）=&gt; console.log（'a=',a,'b=',b,'theArgs=',theArgs）;</p><p class="daima">　 undefined</p><p class="daima">　　test（1,2,3,4,5）;</p><p class="daima">　 a= 1 b= 2 theArgs= [ 3, 4, 5 ]</p><p class="content">这样，我们并不把它写进控制台本身，而是将它存储在一个数组中。如果在测试下面的代码调用了console.log三次：</p><p class="daima">console.log（'First message', 'is', 123）;</p><p class="daima">console.log（'Second message'）;</p><p class="daima">console.log（'Third message'）;</p><p class="content">我们期望_logs字段中包含一个数组['First message is 123', 'Second message', 'Third message']。</p><p class="listTitle1 biaoti8">15.10.2 安装ConsoleSpy</p><p class="content">为了在测试中使用探子，我们声明了两个变量：originalConsole和fakeConsole。前者存放一份原始控制台实例的引用，后者则存放控制台的<b>模拟</b>版本。我们还声明了一些有助于测试input和form元素的变量。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events.spec.ts</b></p><p class="daima">describe（'DemoFormWithEvents',（）=&gt; {</p><p class="daima">　let originalConsole, fakeConsole;</p><p class="daima">　let el, input, form;</p><p class="content">下面可以安装这个伪控制台，指定提供者。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events.spec.ts</b></p><p class="daima">　beforeEach（（）=&gt; {</p><p class="daima">　　// replace the real window.console with our spy</p><p class="daima">　　fakeConsole = new ConsoleSpy（）;</p><p class="daima">　　originalConsole = window.console;</p><p class="daima">　　（&lt;any&gt;window）.console = fakeConsole;</p><p class="daima">　　TestBed.configureTestingModule（{</p><p class="daima">　　　imports：[ FormsModule, ReactiveFormsModule ],</p><p class="daima">　　　declarations：[ DemoFormWithEvents ]</p><p class="daima">　　}）;</p><p class="daima">　}）;</p><p class="content">回到测试代码，下面要做的是将真实控制台替换成我们的模板版本，换掉原始实例。</p><p class="content">最后，在afterAll方法中将原始控制台实例还原，以免对其他测试造成<b>泄漏</b>（leak）。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events.spec.ts</b></p><p class="daima">　// restores the real console</p><p class="daima">　afterAll（（）=&gt;（&lt;any&gt;window）.console = originalConsole）;</p><p class="listTitle1 biaoti8">15.10.3 配置测试模块</p><p class="content">注意，我们在beforeEach块中调用了TestBed.configureTestingModule。要记住configureTestingModule方法为测试设置了根NgModule。</p><p class="content">我们在本例中导入了两个表单模块，声明了一个DemoFormWithEvents组件。</p><p class="content">现在我们可以操控控制台了，下面开始测试表单。</p><p class="listTitle1 biaoti8">15.10.4 测试表单</p><p class="content">现在需要对验证错误信息和表单事件进行测试。</p><p class="content">首先要做的是取得SKU输入字段和表单元素的引用。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events_bad.spec.ts</b></p><p class="daima">　it（'validates and triggers events', fakeAsync（（tcb）=&gt; {</p><p class="daima">　　let fixture = TestBed.createComponent（DemoFormWithEvents）;</p><p class="daima">　　let el = fixture.debugElement.nativeElement;</p><p class="daima">　　let input = fixture.debugElement.query（By.css（'input'））.nativeElement;</p><p class="daima">　　let form = fixture.debugElement.query（By.css（'form'））.nativeElement;</p><p class="daima">　　fixture.detectChanges（）;</p><p class="content">最后一行通知Angular提交所有未完成的变更，正如我们在15.8节所做的那样。接下来将SKU输入值设置为空字符串。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events_bad.spec.ts</b></p><p class="daima">　input.value = '';</p><p class="daima">　dispatchEvent（input, 'input'）;</p><p class="daima">　fixture.detectChanges（）;</p><p class="daima">　tick（）;</p><p class="content">这里我们用dispatchEvent通知Angular输入元素发生了变更，再一次触发变更检测。最后用tick（）确保此时已触发的所有异步代码都已经执行。</p><p class="content">在这个测试中使用fakeAsync和tick的目的就是为了确保表单事件被触发。如果使用async和inject替代，就必须在事件被触发前运行所有代码。</p><p class="content">现在我们已经修改了输入值，需要确保表单验证生效。（使用el变量）查询组件元素，寻找是错误信息的所有子元素，并确保错误信息已经显示。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events_bad.spec.ts</b></p><p class="daima">　let msgs = el.querySelectorAll（'.ui.error.message'）;</p><p class="daima">　expect（msgs[0].innerHTML）.toContain（'SKU is invalid'）;</p><p class="daima">　expect（msgs[1].innerHTML）.toContain（'SKU is required'）;</p><p class="content">接下来依葫芦画瓢，不过这次在SKU字段输入一个值。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events_bad.spec.ts</b></p><p class="daima">　input.value = 'XYZ';</p><p class="daima">　dispatchEvent（input, 'input'）;</p><p class="daima">　fixture.detectChanges（）;</p><p class="daima">　tick（）;</p><p class="content">确保所有的错误信息消失。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events_bad.spec.ts</b></p><p class="daima">　msgs = el.querySelectorAll（'.ui.error.message'）;</p><p class="daima">　expect（msgs.length）.toEqual（0）;</p><p class="content">最后，我们触发表单的提交事件。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events_bad.spec.ts</b></p><p class="daima">　fixture.detectChanges（）;</p><p class="daima">　dispatchEvent（form, 'submit'）;</p><p class="daima">　tick（）;</p><p class="content">最终，我们要确保在提交表单时，通过检查打印到控制台的日志信息来确定事件被触发。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events_bad.spec.ts</b></p><p class="daima">　// checks for the form submitted message</p><p class="daima">　expect（fakeConsole.logs）.toContain（'you submitted value：XYZ'）;</p><p class="content">我们可以继续为另外两个事件添加新的检验：SKU变更和表单变更。然而，我们的测试代码越来越冗长。</p><p class="content">运行测试，可以看到测试通过，如图15-2所示。</p><p class="center"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t17032/306/2654872905/29858/b08673a4/5b03d541N33347433.jpg" class="enlarge" alt="" href="./image/a-145.jpg" /></p><p class="descrip"><b>图15-2　DemoFormWithEvents测试输出</b></p><p class="content">测试本身没有问题，但我们在代码风格上闻到了一些坏味道：</p><p class="content">●一个超长的it条件（超过5～10行）；</p><p class="content">●每个it中不止一两个expect；</p><p class="content">●测试描述中使用了and一词。</p><p class="listTitle1 biaoti8">15.10.5 重构表单测试</p><p class="content">解决问题的第一步就是将创建组件、获取组件元素和用于输入和表单元素的代码从中抽取出来。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events.spec.ts</b></p><p class="daima">　function createComponent（）：ComponentFixture&lt;any&gt; {</p><p class="daima">　　let fixture = TestBed.createComponent（DemoFormWithEvents）;</p><p class="daima">　　el = fixture.debugElement.nativeElement;</p><p class="daima">　　input = fixture.debugElement.query（By.css（'input'））.nativeElement;</p><p class="daima">　　form = fixture.debugElement.query（By.css（'form'））.nativeElement;</p><p class="daima">　　fixture.detectChanges（）;</p><p class="daima">　　return fixture;</p><p class="daima">　}</p><p class="content">createComponent代码相当简明：使用TestBed.createComponent创建组件，获取所有元素并调用detectChanges。</p><p class="content">现在第一个要测试的是，提供一个空的SKU字段，我们应该看到两条错误信息。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events.spec.ts</b></p><p class="daima">　it（'displays errors with no sku', fakeAsync（（）=&gt; {</p><p class="daima">　　let fixture = createComponent（）;</p><p class="daima">　　input.value = '';</p><p class="daima">　　dispatchEvent（input, 'input'）;</p><p class="daima">　　fixture.detectChanges（）;</p><p class="daima">　　// no value on sku field, all error messages are displayed</p><p class="daima">　　let msgs = el.querySelectorAll（'.ui.error.message'）;</p><p class="daima">　　expect（msgs[0].innerHTML）.toContain（'SKU is invalid'）;</p><p class="daima">　　expect（msgs[1].innerHTML）.toContain（'SKU is required'）;</p><p class="daima">　}））;</p><p class="content">如你所见，代码清晰了很多。测试很专注，而且只测试一件事。太棒了！</p><p class="content">在新的结构中添加第二个测试也很简单。这次要测试的是，一旦给SKU字段赋值，错误信息就会消失。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events.spec.ts</b></p><p class="daima">　it（'displays no errors when sku has a value', fakeAsync（（）=&gt; {</p><p class="daima">　　let fixture = createComponent（）;</p><p class="daima">　　input.value = 'XYZ';</p><p class="daima">　　dispatchEvent（input, 'input'）;</p><p class="daima">　　fixture.detectChanges（）;</p><p class="daima">　　let msgs = el.querySelectorAll（'.ui.error.message'）;</p><p class="daima">　　expect（msgs.length）.toEqual（0）;</p><p class="daima">　}））;</p><p class="content">你可能注意到了一点：到目前为止，我们的测试代码并没有使用fakeAsync，而是使用async和inject替代。</p><p class="content">这次重构的另一个好处是，仅在当我们检查是否有信息发送到控制台时才使用fakeAsync和tick（），因为这正是由表单事件处理器负责的。</p><p class="content">下一个测试恰恰是：当SKU值发生变更时，我们应该有一条信息发送到控制台。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events.spec.ts</b></p><p class="daima">　it（'handles sku value changes', fakeAsync（（）=&gt; {</p><p class="daima">　　let fixture = createComponent（）;</p><p class="daima">　　input.value = 'XYZ';</p><p class="daima">　　dispatchEvent（input, 'input'）;</p><p class="daima">　　tick（）;</p><p class="daima">　　expect（fakeConsole.logs）.toContain（'sku changed to：XYZ'）;</p><p class="daima">　}））;</p><p class="content">对于表单变更进行同样的处理。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events.spec.ts</b></p><p class="daima">　it（'handles form changes', fakeAsync（（）=&gt; {</p><p class="daima">　　let fixture = createComponent（）;</p><p class="daima">　　input.value = 'XYZ';</p><p class="daima">　　dispatchEvent（input, 'input'）;</p><p class="daima">　　tick（）;</p><p class="daima">　　expect（fakeConsole.logs）.toContain（'form changed to：[object Object]'）;</p><p class="daima">　}））;</p><p class="content">对于表单提交事件也进行同样的处理。</p><p class="reference"><b>code/forms/test/forms/demo_form_with_events.spec.ts</b></p><p class="daima">　it（'handles form submission', fakeAsync（（tcb）=&gt; {</p><p class="daima">　　let fixture = createComponent（）;</p><p class="daima">　　input.value = 'ABC';</p><p class="daima">　　dispatchEvent（input, 'input'）;</p><p class="daima">　　tick（）;</p><p class="daima">　　fixture.detectChanges（）;</p><p class="daima">　　dispatchEvent（form, 'submit'）;</p><p class="daima">　　tick（）;</p><p class="daima">　　expect（fakeConsole.logs）.toContain（'you submitted value：ABC'）;</p><p class="daima">　}））;</p><p class="content">再次运行测试，会得到更清晰的输出结果，如图15-3所示。</p><p class="center"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t18826/7/2698841301/65827/4677d707/5b03d541N8fbda57d.jpg" class="enlarge" alt="" href="./image/a-146.jpg" /></p><p class="descrip"><b>图15-3　重构后的DemoFormWithEvents测试输出</b></p><p class="content">这次重构的另外一个好处是，出错时一眼就可以看出来。回到组件代码，在提交表单时更改消息，从而强制一个测试失败。</p><p class="daima">onSubmit（form：any）：void {</p><p class="daima">　console.log（'you have submitted the value：', form.sku）;</p><p class="daima">}</p><p class="content">如果运行之前版本的测试，可能看到如图15-4所示的结果。</p><p class="center"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t20815/11/207547691/72241/29d2597b/5b03d540N31700184.jpg" class="enlarge" alt="" href="./image/a-147.jpg" /></p><p class="descrip"><b>图15-4　重构前的DemoFormWithEvents错误输出</b></p><p class="content">它不会立即显示失败的原因所在。我们必须通过错误代号来明白提交的信息失败了。我们也不能肯定这是破坏组件的唯一事件，因为还可能有其他测试条件在遭遇失败时根本没有机会运行。</p><p class="content">现在比较一下在重构过的代码上得到的错误信息，如图15-5所示。</p><p class="center"><img src="https://img30.360buyimg.com/ebookadmin/jfs/t17176/42/2625308460/85975/11015890/5b03d540N9bb7cec6.jpg" class="enlarge" alt="" href="./image/a-148.jpg" /></p><p class="descrip"><b>图15-5　重构后的DemoFormWithEvents错误输出</b></p><p class="content">这个版本很清晰，唯一失败的是表单提交事件。</p><p class="sectionTitle biaoti3"><a id="sec011"></a>15.11 测试HTTP请求</p><p class="content">我们可以采用与之前相同的策略来测试HTTP交互：写一个Http类的模拟版本，因为它是一个外部依赖。</p><p class="content">但是因为绝大多数使用Angular编写的单页面程序都是使用HTTP与API交互的，所以Angular测试类库已经提供了一个内置的替代品：MockBackend。</p><p class="content">本章之前测试SpotifyService类时已经用到过这个类。</p><p class="content">现在继续深入，见识一下更多的测试场景，也可以获得更好的编程实践。为了实现这个目的，我们为第6章的例子编写测试。</p><p class="content">首先，一起看看如何测试不同的HTTP方法，如POST和DELETE，还有如何测试正要发送正确的HTTP头信息。</p><p class="content">回到第6章，我们创建的这个实例包括了如何使用Http实现达到目的。</p><p class="listTitle1 biaoti8">15.11.1 测试POST方法</p><p class="content">第一个要写的测试是确保makePost方法发送一条正确的POST请求。</p><p class="reference"><b>code/http/app/ts/components/MoreHTTPRequests.ts</b></p><p class="daima">　makePost（）：void {</p><p class="daima">　　this.loading = true;</p><p class="daima">　　this.http.post（</p><p class="daima">　　　'http：//jsonplaceholder.typicode.com/posts',</p><p class="daima">　　　JSON.stringify（{</p><p class="daima">　　　　body：'bar',</p><p class="daima">　　　　title：'foo',</p><p class="daima">　　　　userId：1</p><p class="daima">　　　}））</p><p class="daima">　　　.subscribe（（res：Response）=&gt; {</p><p class="daima">　　　　this.data = res.json（）;</p><p class="daima">　　　　this.loading = false;</p><p class="daima">　　　}）;</p><p class="daima">　}</p><p class="content">为这个方法编写测试时，目的是测试两点：</p><p class="content">（1）请求方法（POST）是正确的；</p><p class="content">（2）目标URL也是正确的。</p><p class="content">下面把这个想法变成测试。</p><p class="reference"><b>code/http/test/MoreHTTPRequests.spec.ts</b></p><p class="daima">　it（'performs a POST',</p><p class="daima">　　async（inject（[MockBackend],（backend）=&gt; {</p><p class="daima">　　　let fixture = TestBed.createComponent（MoreHTTPRequests）;</p><p class="daima">　　　let comp = fixture.debugElement.componentInstance;</p><p class="daima">　　　backend.connections.subscribe（c =&gt; {</p><p class="daima">　　　　expect（c.request.url）</p><p class="daima">　　　　　.toBe（'http：//jsonplaceholder.typicode.com/posts'）;</p><p class="daima">　　　　expect（c.request.method）.toBe（RequestMethod.Post）;</p><p class="daima">　　　　c.mockRespond（new Response（&lt;any&gt;<b>ody：'{"response"："OK"</b>'}））;</p><p class="daima">　　　}）;</p><p class="daima">　　　comp.makePost（）;</p><p class="daima">　　　expect（comp.data）.toEqual（{'response'：'OK'}）;</p><p class="daima">　　}））</p><p class="daima">　）;</p><p class="content">注意，可以在backend.connections上调用subscribe方法。每当有新连接建立时就会触发我们的代码，这提供了检查请求内容的机会，并可以按预期设置响应结果。</p><p class="content">这里，你也可以：</p><p class="content">●添加请求断言语句，比如检查请求的URL和HTTP方法是否正确；</p><p class="content">●设置模拟的响应结果，强制代码根据不同的测试场景作出不同的响应。</p><p class="content">Angular使用一个名为RequestMethod的enum来判别不同的HTTP方法。这里是支持的方法。</p><p class="daima">export enum RequestMethod {</p><p class="daima">　Get,</p><p class="daima">　Post,</p><p class="daima">　Put,</p><p class="daima">　Delete,</p><p class="daima">　Options,</p><p class="daima">　Head,</p><p class="daima">　Patch</p><p class="daima">}</p><p class="content">最后，在调用makePost（）后，我们再次检查以确保预设的响应就是分配给组件的那个。</p><p class="content">现在我们理解了其工作原理，针对DELETE方法增加一个测试并不难。</p><p class="listTitle1 biaoti8">15.11.2 测试DELETE方法</p><p class="content">这是makeDelete方法的具体实现。</p><p class="reference"><b>code/http/app/ts/components/MoreHTTPRequests.ts</b></p><p class="daima">　makeDelete（）：void {</p><p class="daima">　　this.loading = true;</p><p class="daima">　　this.http.delete（'http：//jsonplaceholder.typicode.com/posts/1'）</p><p class="daima">　　　.subscribe（（res：Response）=&gt; {</p><p class="daima">　　　　this.data = res.json（）;</p><p class="daima">　　　　this.loading = false;</p><p class="daima">　　　}）;</p><p class="daima">　}</p><p class="content">这是我们用来测试它的代码。</p><p class="reference"><b>code/http/test/MoreHTTPRequests.spec.ts</b></p><p class="daima">　it（'performs a DELETE',</p><p class="daima">　　async（inject（[MockBackend],（backend）=&gt; {</p><p class="daima">　　　let fixture = TestBed.createComponent（MoreHTTPRequests）;</p><p class="daima">　　　let comp = fixture.debugElement.componentInstance;</p><p class="daima">　　　backend.connections.subscribe（c =&gt; {</p><p class="daima">　　　　expect（c.request.url）</p><p class="daima">　　　　　.toBe（'http：//jsonplaceholder.typicode.com/posts/1'）;</p><p class="daima">　　　　expect（c.request.method）.toBe（RequestMethod.Delete）;</p><p class="daima">　　　　c.mockRespond（new Response（&lt;any&gt;<b>ody：'{"response"："OK"</b>'}））;</p><p class="daima">　　　}）;</p><p class="daima">　　　comp.makeDelete（）;</p><p class="daima">　　　expect（comp.data）.toEqual（{'response'：'OK'}）;</p><p class="daima">　　}））</p><p class="daima">　）;</p><p class="content">除了URL和HTTP方法（这里使用RequestMethod.Delete）稍有不同，代码并无太大的差异。</p><p class="listTitle1 biaoti8">15.11.3 测试HTTP头</p><p class="content">针对这个类，最后一个要测试的方法是makeHeaders。</p><p class="reference"><b>code/http/app/ts/components/MoreHTTPRequests.ts</b></p><p class="daima">　makeHeaders（）：void {</p><p class="daima">　　let headers：Headers = new Headers（）;</p><p class="daima">　　headers.append（'X-API-TOKEN', 'ng-book'）;</p><p class="daima">　　let opts：RequestOptions = new RequestOptions（）;</p><p class="daima">　　opts.headers = headers;</p><p class="daima">　　this.http.get（'http：//jsonplaceholder.typicode.com/posts/1', opts）</p><p class="daima">　　　.subscribe（（res：Response）=&gt; {</p><p class="daima">　　　　this.data = res.json（）;</p><p class="daima">　　　}）;</p><p class="daima">　}</p><p class="content">在本例中，我们的测试应该集中在确保X-API-TOKEN头被正确地设置为ng-book。</p><p class="reference"><b>code/http/test/MoreHTTPRequests.spec.ts</b></p><p class="daima">　it（'sends correct headers',</p><p class="daima">　　async（inject（[MockBackend],（backend）=&gt; {</p><p class="daima">　　　let fixture = TestBed.createComponent（MoreHTTPRequests）;</p><p class="daima">　　　let comp = fixture.debugElement.componentInstance;</p><p class="daima">　　　backend.connections.subscribe（c =&gt; {</p><p class="daima">　　　　expect（c.request.url）</p><p class="daima">　　　　　.toBe（'http：//jsonplaceholder.typicode.com/posts/1'）;</p><p class="daima">　　　　expect（c.request.headers.has（'X-API-TOKEN'））.toBeTruthy（）;</p><p class="daima">　　　　expect（c.request.headers.get（'X-API-TOKEN'））.toEqual（'ng-book'）;</p><p class="daima">　　　　c.mockRespond（new Response（&lt;any&gt;<b>ody：'{"response"："OK"</b>'}））;</p><p class="daima">　　　}）;</p><p class="daima">　　　comp.makeHeaders（）;</p><p class="daima">　　　expect（comp.data）.toEqual（{'response'：'OK'}）;</p><p class="daima">　　}））</p><p class="daima">　）;</p><p class="content">请求连接的request.headers属性会返回一个Headers实例。我们使用两个方法执行两个不同的断言：</p><p class="content">●has方法检查指定的头是否已经设置，忽略其值；</p><p class="content">●get方法返回设置的值。</p><p class="content">如果只检查是否设置了头即可，使用has。如果需要检测其设置的值，要使用get。</p><p class="content">到此为止，我们完成了Angular中不同HTTP方法和头的测试。现在转向一个更为复杂的例子，它与你在编写真实程序时遇到的场景非常接近。</p><p class="listTitle1 biaoti8">15.11.4 测试YouTubeService</p><p class="content">我们在第6章构建的另外一个实例是YouTube视频搜索。在这个实例中，HTTP交互过程包含在YouTubeService类中。</p><p class="reference"><b>code/http/app/ts/components/YouTubeSearchComponent.ts</b></p><p class="daima">/**</p><p class="daima">* YouTubeService connects to the YouTube API</p><p class="daima">* See：* https：//developers.google.com/youtube/v3/docs/search/list</p><p class="daima">*/</p><p class="daima">@Injectable（）</p><p class="daima">export class YouTubeService {</p><p class="daima">　constructor（private http：Http,</p><p class="daima">　　　　　　　@Inject（YOUTUBE_API_KEY）private apiKey：string,</p><p class="daima">　　　　　　　@Inject（YOUTUBE_API_URL）private apiUrl：string）{</p><p class="daima">　}</p><p class="daima">　search（query：string）：Observable&lt;SearchResult[]&gt; {</p><p class="daima">　　let params：string = [</p><p class="daima">　　　`q=${query}`,</p><p class="daima">　　　`key=${this.apiKey}`,</p><p class="daima">　　　`part=snippet`,</p><p class="daima">　　　`type=video`,</p><p class="daima">　　　`maxResults=10`</p><p class="daima">　　].join（'&amp;'）;</p><p class="daima">　　let queryUrl：string = `${this.apiUrl}?${params}`;</p><p class="daima">　　return this.http.get（queryUrl）</p><p class="daima">　　　.map（（response：Response）=&gt; {</p><p class="daima">　　　　return（&lt;any&gt;response.json（））.items.map（item =&gt; {</p><p class="daima">　　　　　// console.log（"raw item", item）; // uncomment if you want to debug</p><p class="daima">　　　　　return new SearchResult（{</p><p class="daima">　　　　　　id：item.id.videoId,</p><p class="daima">　　　　　　title：item.snippet.title,</p><p class="daima">　　　　　　description：item.snippet.description,</p><p class="daima">　　　　　　thumbnailUrl：item.snippet.thumbnails.high.url</p><p class="daima">　　　　　}）;</p><p class="daima">　　　　}）;</p><p class="daima">　　　}）;</p><p class="daima">　}</p><p class="daima">}</p><p class="content">它利用YouTube API搜索视频，解析结果并保存到一个SearchResult实例中。</p><p class="reference"><b>code/http/app/ts/components/YouTubeSearchComponent.ts</b></p><p class="daima">class SearchResult {</p><p class="daima">　id：string;</p><p class="daima">　title：string;</p><p class="daima">　description：string;</p><p class="daima">　thumbnailUrl：string;</p><p class="daima">　videoUrl：string;</p><p class="daima">　constructor（obj?：any）{</p><p class="daima">　　this.id　　　　　　　= obj &amp;&amp; obj.id　　　　　　 || null;</p><p class="daima">　　this.title　　　　　 = obj &amp;&amp; obj.title　　　　　|| null;</p><p class="daima">　　this.description　　 = obj &amp;&amp; obj.description　　|| null;</p><p class="daima">　　this.thumbnailUrl　　= obj &amp;&amp; obj.thumbnailUrl　 || null;</p><p class="daima">　　this.videoUrl　　　　= obj &amp;&amp; obj.videoUrl　　　 ||</p><p class="daima">　　　　　　　　　　　　　　 `https：//www.youtube.com/watch?v=${this.id}`;</p><p class="daima">　}</p><p class="daima">}</p><p class="content">我们需要测试这个服务的几个重要方面：</p><p class="content">●给定一个JSON响应，服务能够分析出视频的id、标题（title）、描述（description）和缩略图（thumbnail）属性；</p><p class="content">●我们请求的URL使用了提供的搜索关键字；</p><p class="content">●URL前缀设置在YOUTUBE_API_URL常量中；</p><p class="content">●使用的API键值与YOUTUBE_API_KEY常量匹配。</p><p class="content">记住这些，开始编写测试。</p><p class="reference"><b>code/http/test/YouTubeSearchComponentBefore.spec.ts</b></p><p class="daima">　describe（'MoreHTTPRequests（before）',（）=&gt; {</p><p class="daima">　　beforeEach（（）=&gt; {</p><p class="daima">　　　TestBed.configureTestingModule（{</p><p class="daima">　　　　providers：[</p><p class="daima">　　　　　YouTubeService,</p><p class="daima">　　　　　BaseRequestOptions,</p><p class="daima">　　　　　MockBackend,</p><p class="daima">　　　　　{ provide：YOUTUBE_API_KEY, useValue：'YOUTUBE_API_KEY' },</p><p class="daima">　　　　　{ provide：YOUTUBE_API_URL, useValue：'YOUTUBE_API_URL' },</p><p class="daima">　　　　　{ provide：Http,</p><p class="daima">　　　　　　useFactory：（backend：ConnectionBackend,</p><p class="daima">　　　　　　　　　　　　 defaultOptions：BaseRequestOptions）=&gt; {</p><p class="daima">　　　　　　　　　　　　　 return new Http（backend, defaultOptions）;</p><p class="daima">　　　　　　　　　　　　 }, deps：[MockBackend, BaseRequestOptions] }</p><p class="daima">　　　　]</p><p class="daima">　　}）;</p><p class="daima">　}）;</p><p class="content">和之前写测试的准备工作一样，首先配置依赖：这里我们使用真实的YouTubeService，但YOUTUBE_API_KEY和YOUTUBE_API_URL使用伪值。同时配置Http类使用一个MockBackend类。</p><p class="content">现在，开始写第一个测试用例。</p><p class="reference"><b>code/http/test/YouTubeSearchComponentBefore.spec.ts</b></p><p class="daima">　describe（'search',（）=&gt; {</p><p class="daima">　　it（'parses YouTube response',</p><p class="daima">　　　inject（[YouTubeService, MockBackend], fakeAsync（（service, backend）=&gt; {</p><p class="daima">　　　　let res;</p><p class="daima">　　　　backend.connections.subscribe（c =&gt; {</p><p class="daima">　　　　　c.mockRespond（new Response（&lt;any&gt;{</p><p class="daima">　　　　　　body：`</p><p class="daima">　　　　　　{</p><p class="daima">　　　　　　　"items"：[</p><p class="daima">　　　　　　　{</p><p class="daima">　　　　　　　　"id"：{ "videoId"："VIDEO_ID" },</p><p class="daima">　　　　　　　　"snippet"：{</p><p class="daima">　　　　　　　　　"title"："TITLE",</p><p class="daima">　　　　　　　　　"description"："DESCRIPTION",</p><p class="daima">　　　　　　　　　"thumbnails"：{</p><p class="daima">　　　　　　　　　　"high"：{ "url"："THUMBNAIL_URL" }</p><p class="daima">　　　　　　　　　 }}}]}`</p><p class="daima">　　　　　}））;</p><p class="daima">　　　　}）;</p><p class="daima">　　　　service.search（'hey'）.subscribe（_res =&gt; {</p><p class="daima">　　　　　res = _res;</p><p class="daima">　　　　}）;</p><p class="daima">　　　　tick（）;</p><p class="daima">　　　　let video = res[0];</p><p class="daima">　　　　expect（video.id）.toEqual（'VIDEO_ID'）;</p><p class="daima">　　　　expect（video.title）.toEqual（'TITLE'）;</p><p class="daima">　　　　expect（video.description）.toEqual（'DESCRIPTION'）;</p><p class="daima">　　　　expect（video.thumbnailUrl）.toEqual（'THUMBNAIL_URL'）;</p><p class="daima">　　　}））</p><p class="daima">　　）</p><p class="daima">　}）;</p><p class="content">这里我们通知Http返回一个伪响应结果，与调用真实URL时期望YouTube API返回响应结果的相关字段一致。这可以通过调用连接的mockRespond方法实现。</p><p class="reference"><b>code/http/test/YouTubeSearchComponentBefore.spec.ts</b></p><p class="daima">　　　　service.search（'hey'）.subscribe（_res =&gt; {</p><p class="daima">　　　　　res = _res;</p><p class="daima">　　　　}）;</p><p class="daima">　　　　tick（）;</p><p class="content">接下来调用我们要测试的方法：search。调用时使用关键字hey，并抓取响应结果保存在res变量中。</p><p class="content">你之前可能注意到了，我们使用的是fakeAsync，它需要手动调用tick（）来同步异步代码。这里沿用这种模式，期望搜索完成了执行过程，并且res已经保存了结果。</p><p class="content">现在就可以验证结果值了。</p><p class="reference"><b>code/http/test/YouTubeSearchComponentBefore.spec.ts</b></p><p class="daima">　　　　let video = res[0];</p><p class="daima">　　　　expect（video.id）.toEqual（'VIDEO_ID'）;</p><p class="daima">　　　　expect（video.title）.toEqual（'TITLE'）;</p><p class="daima">　　　　expect（video.description）.toEqual（'DESCRIPTION'）;</p><p class="daima">　　　　expect（video.thumbnailUrl）.toEqual（'THUMBNAIL_URL'）;</p><p class="content">从响应列表中读取第一个元素。我们已知它是SearchResult，现在要基于之前预设的响应结果检查每个属性设置正确无误：id、标题、描述和缩略图URL应该全部匹配。</p><p class="content">至此，我们完成了写测试的第一个目标。然而，刚才不是说使用一个超大it的方法并使用太多的expect产生了代码坏味道吗？</p><p class="content">的确是，所以在继续前进之前，先对代码进行重构，从而能更容易地使用单独的断言。</p><p class="content">在describe（'search', ……）内部添加以下辅助函数。</p><p class="reference"><b>code/http/test/YouTubeSearchComponentAfter.spec.ts</b></p><p class="daima">　　function search（term：string, response：any, callback）{</p><p class="daima">　　　return inject（[YouTubeService, MockBackend],</p><p class="daima">　　　　fakeAsync（（service, backend）=&gt; {</p><p class="daima">　　　　　var req;</p><p class="daima">　　　　　var res;</p><p class="daima">　　　　　backend.connections.subscribe（c =&gt; {</p><p class="daima">　　　　　　req = c.request;</p><p class="daima">　　　　　　c.mockRespond（new Response（&lt;any&gt;<b>ody：response</b>））;</p><p class="daima">　　　　　}）;</p><p class="daima">　　　　　service.search（term）.subscribe（_res =&gt; {</p><p class="daima">　　　　　　res = _res;</p><p class="daima">　　　　　}）;</p><p class="daima">　　　　　tick（）;</p><p class="daima">　　　　　callback（req, res）;</p><p class="daima">　　　　}）</p><p class="daima">　　　）</p><p class="daima">　　}</p><p class="content">一起看看这个函数如何工作：它使用inject和fakeAsync完成了与之前同样的任务，不同的是它使用了一种配置的方式。我们提供了一个<b>搜索关键字</b>、一个<b>响应</b>和一个<b>回调函数</b>。有了这些参数，我们使用搜索关键字调用search方法，设置好伪响应结果，并在完成请求时调用回调函数，从而提供请求和响应对象。</p><p class="content">使用这种方法，测试只需要调用这个函数并检查其中一个对象即可。</p><p class="content">将之前写的测试拆分成四个测试，每个测试针对一种不同的响应结果。</p><p class="reference"><b>code/http/test/YouTubeSearchComponentAfter.spec.ts</b></p><p class="daima">　　it（'parses YouTube video id', search（'hey', response,（req, res）=&gt; {</p><p class="daima">　　　let video = res[0];</p><p class="daima">　　　expect（video.id）.toEqual（'VIDEO_ID'）;</p><p class="daima">　　}））;</p><p class="daima">　　it（'parses YouTube video title', search（'hey', response,（req, res）=&gt; {</p><p class="daima">　　　let video = res[0];</p><p class="daima">　　　expect（video.title）.toEqual（'TITLE'）;</p><p class="daima">　　}））;</p><p class="daima">　　it（'parses YouTube video description', search（'hey', response,（req, res）=&gt;\</p><p class="daima">{</p><p class="daima">　　　let video = res[0];</p><p class="daima">　　　expect（video.description）.toEqual（'DESCRIPTION'）;</p><p class="daima">　　}））;</p><p class="daima">　　it（'parses YouTube video thumbnail', search（'hey', response,（req, res）=&gt; {</p><p class="daima">　　　let video = res[0];</p><p class="daima">　　　expect（video.description）.toEqual（'DESCRIPTION'）;</p><p class="daima">　　}））;</p><p class="content">看起来不错吧？这个小而且专注的测试只有一个测试目的。太棒了！</p><p class="content">现在为余下的目标添加测试代码应该很容易了。</p><p class="reference"><b>code/http/test/YouTubeSearchComponentAfter.spec.ts</b></p><p class="daima">　　it（'sends the query', search（'term', response,（req, res）=&gt; {</p><p class="daima">　　　expect（req.url）.toContain（'q=term'）;</p><p class="daima">　　}））;</p><p class="daima">　　it（'sends the API key', search（'term', response,（req, res）=&gt; {</p><p class="daima">　　　expect（req.url）.toContain（'key=YOUTUBE_API_KEY'）;</p><p class="daima">　　}））;</p><p class="daima">　　it（'uses the provided YouTube URL', search（'term', response,（req, res）=&gt; {</p><p class="daima">　　　expect（req.url）.toMatch（/^YOUTUBE_API_URL\?/）;</p><p class="daima">　　}））;</p><p class="content">你可以按照自己的想法随意加入更多的测试。比如，针对响应结果中包含多个条目并有不同的属性添加一个测试。看看代码中是否有你想进行测试的其他方面。</p><p class="sectionTitle biaoti3"><a id="sec012"></a>15.12 总结</p><p class="content">Angular团队在为Angular提供测试功能方面做了大量的工作。这样我们才能轻松地测试应用程序的方方面面：从控制器到服务类、表单和HTTP。本来棘手的异步代码测试现在也不费吹灰之力。</p><ol class="mz-footnote"><li id="footnote1">Red-Green-Refactor，是一种标准的测试驱动开发流程。——译者注</li><li id="footnote2">https：//angular.github.io/protractor/#/</li><li id="footnote3">http：//jasmine.github.io/2.4/introduction.html</li><li id="footnote4">https：//developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/rest_parameters</li><li id="footnote5">https：//nodejs.org/en/</li></ol></body></html>